Click here to Skip to main content
Click here to Skip to main content

WatinN to Automate Browser and Test Sophisticated ASP.NET AJAX Sites

, 11 Jun 2011 CPOL
Rate this:
Please Sign up or sign in to vote.
WatinN to Automate Browser and Test Sophisticated ASP.NET AJAX Sites
TestControlAdapterOpt.gif

Introduction

WatiN is a great .NET library for writing automated browser based tests that uses real browser to go to websites, perform actions and check for browser output. Combined with a unit test library like xUnit, you can use WatiN to perform automated regression tests on your websites and save many hours of manual testing every release. Moreover, WatiN can be used to stress test JavaScripts on the page as it can push the browser to perform operations repeatedly and measure how long it takes for Javascripts to run. Thus you can test your Javascripts for performance, rendering speed of your website and ensure the overall presentation is fast and smooth for users.

I have written some extension methods for WatiN to help facilitate AJAX related tests, especially with ASP.NET UpdatePanel, jQuery and dealing with element positions. I will show you how to use these libraries to test sophisticated AJAX websites, like the one I have built - Dropthings, which is a widget powered ASP.NET AJAX portal using ASP.NET UpdatePanel, jQuery to create a Web 2.0 presentation. You can simulate and test UpdatePanel updates, AJAX calls and UI update and even drag & drop of widgets!

You can see the implementation of automated tests in my open source project codebase.

WatiN Extension for ASP.NET AJAX and jQuery

When you are testing an ASP.NET AJAX website that uses the UpdatePanel, you need to make sure async postbacks are successfully completed before testing the page output for correctness. For example, you clicked a button using WatiN and that causes an UpdatePanel to update the page. You need to wait for the async postback to complete before you can test for the correct output. Here’s a code snippet that does it:

internal static bool WaitForAsyncPostbackComplete(this Browser browser, int timeout)
{
    int timeWaitedInMilliseconds = 0;
    var maxWaitTimeInMilliseconds = Settings.WaitForCompleteTimeOut * 1000;
    var scriptToCheck = 
	"Sys.WebForms.PageRequestManager.getInstance().get_isInAsyncPostBack();";

    while (bool.Parse(browser.Eval(scriptToCheck)) == true
            && timeWaitedInMilliseconds < maxWaitTimeInMilliseconds)
    {
        Thread.Sleep(Settings.SleepTime);
        timeWaitedInMilliseconds += Settings.SleepTime;
    }

    return bool.Parse(browser.Eval(scriptToCheck));
}

Similarly for jQuery calls, if you are making AJAX calls using the jQuery’s default implementation, then you need to ensure the Ajax calls completed before you test for any UI update. The way to do it is first call this right after your page is loaded so that it installs a hook to monitor Ajax calls:

internal static void InjectJQueryAjaxMonitor(this Browser browser)
{
    const string monitorScript =
        @"function AjaxMonitor(){"
        + "var ajaxRequestCount = 0;"

        + "$(document).ajaxStart(function(){"
        + "    ajaxRequestCount++;"
        + "});"

        + "$(document).ajaxComplete(function(){"
        + "    ajaxRequestCount--;"
        + "});"

        + "this.isRequestInProgress = function(){"
        + "    return (ajaxRequestCount > 0);"
        + "};"
        + "}"

        + "var watinAjaxMonitor = new AjaxMonitor();";

    browser.Eval(monitorScript);
} 

Then use this to wait for Ajax calls to complete:

internal static void WaitForJQueryAjaxRequest(this Browser browser)
{
    int timeWaitedInMilliseconds = 0;
    var maxWaitTimeInMilliseconds = Settings.WaitForCompleteTimeOut * 1000;

    while (browser.IsJQueryAjaxRequestInProgress()
            && timeWaitedInMilliseconds < maxWaitTimeInMilliseconds)
    {
        Thread.Sleep(Settings.SleepTime);
        timeWaitedInMilliseconds += Settings.SleepTime;
    }
}

internal static bool IsJQueryAjaxRequestInProgress(this Browser browser)
{
    var evalResult = browser.Eval("watinAjaxMonitor.isRequestInProgress()");
    return evalResult == "true";
}

Sometimes you have to simulate a user visit for the first time. So, you need clear cache and cookie before you hit the site. Here’s how to do it.

private static void ClearCookiesInIE()
{
    Process.Start("RunDll32.exe", "InetCpl.cpl,ClearMyTracksByProcess 2").WaitForExit();
}

private static void DeleteEverythingInIE()
{
    Process.Start("RunDll32.exe", "InetCpl.cpl,ClearMyTracksByProcess 255").WaitForExit();
}

Sometimes you need to know the position of elements on the page so that you can simulate mouse clicks on a certain location on the page. Here’s a snippet that returns the position of the element on the browser.

internal static int[] FindPosition(this Browser browser, Element e)
{
    var top = 0;
    var left = 0;

    var item = e;
    while (item != null)
    {
        top += int.Parse(item.GetAttributeValue("offsetTop"));
        left += int.Parse(item.GetAttributeValue("offsetLeft"));

        item = item.Parent;
    }

    return new int [] { left, top };
}

Now you are ready to write some sophisticated AJAX tests. I will show you some common scenarios like showing content from async update inside UpdatePanel and then verifying the necessary UI elements are there, then adding new items to the page dynamically and checking if they get added properly and even some drag & drop simulated by WatiN.

Creating Page Adapters using WatiN

WatiN has a useful concept called Page Adapter. You can create a class that represents functionalities on the page. For example, the class can expose properties which represent important elements on the page. It can offer functions to perform some important actions on the page. Thus you encapsulate all the logic to interact with the page in a single class and not spread it around hundreds of test methods. For example, the homepage of Dropthings has a link for showing a widget gallery. When clicked, it performs an async update and loads a gallery of widgets on the page.

AddStuffOptPlus.gif

Here’s how a Page Adapter encapsulates these:

[Page(UrlRegex=@"Default\.aspx")]
public class HomePage : Page
{
    [FindBy(Id = "TabControlPanel_ShowAddContentPanel")]
    public Link AddStuffLink;

    [FindBy(ClassRegex = "newtab_add*")]
    public Link AddNewTabLink;

    public void ShowAddStuff()
    {
        AddStuffLink.Click();
    }

    public void AddNewTab()
    {
        AddNewTabLink.Click();
    }

    public Table WidgetDataList
    {
        get
        {
            return base.Document.Table
		("TabControlPanel_WidgetListControlAdd_WidgetDataList");
        }
    }

    public List<Link> AddWidgetLinks
    {
        get
        {
            return base.Document.Links.Where
		(link => link.ClassName == "widgetitem").ToList();
        }
    }

First you define a class that inherits from Page class found in WatiN library, not the ASP.NET Page class. Then you add an attribute [Page(UrlRegex=””)] where you put the page name in Regular Expression format that matches the page. This match is used to identify whether the current URL WatiN is using on the browser matches with the desired page or not. Then you define the public properties that represent different elements on the page. It can be buttons, links, tables, textboxes, etc., any HTML element that WatiN supports. Here I have assigned some properties to represent the “Add Stuff” link that you see on Dropthings. The WidgetDataList represents the HTML table that is generated by a DataGrid on the server side. The AddWidgetLinks represents the collection of links inside the WidgetDataList.

Once you have the page adapter, you can write tests that use this page adapter to click on important elements, then wait for the response, match the response and confirm the response is correct or not.

Writing Tests using WatiN

I have used xUnit and SubSpec to write tests following the Behavior Driven Development BDD) approach. You can read about why I prefer the BDD approach over Test Driven Development (TDD) approach from this article.
[Specification]
public void User_can_show_the_widget_gallery()
{
    var browser = default(Browser);
    var page = default(HomePage);
    "Given a user on the homepage".Context(() =>
    {
        browser = BrowserHelper.OpenNewBrowser(Urls.Homepage);
        page = browser.Page<HomePage>();
    });

    "When user clicks on the 'Add Stuff' link".Do(() =>
    {
        page.ShowAddStuff();
        browser.WaitForAsyncPostbackComplete(10000);
    });

    "It should show the widget gallery".Assert(() =>
    {
        using (browser)
        {
            Assert.True(page.WidgetDataList.Exists);
            Assert.NotEqual(0, page.AddWidgetLinks.Count);
        }
    });
}

The above test uses the HomePage adapter to show the widget gallery and confirm that the gallery is displayed and there’s widget links showing on the page.

If you did not use the Page Adapter approach, then the test code will be stained with DIV IDs, link names, classes, etc. and full of Find.ById, Find.ByClass etc. calls to WatiN library. The Page Adapter completely hides the underlying layout of the page being tested and it makes the test code a lot more readable.

Creating Control Adapter

You can create an adapter for controls on the page, not just the whole page itself. If you have complex controls on the page, like a Login box, a data grid with buttons, a form, a calendar, etc., you can create Control Adapters out of them to make interaction with those controls simpler. The logic to deal with those controls are then encapsulated in a single class and thus not duplicated across multiple test methods.

For example, on Dropthings, each widget is a complex control. It has a header, body, close button, edit button, a title bar, etc. Interacting with these controls is complex because the ID of these elements vary from user to user as these are dynamically created for each user.

WidgetLayout.png

So, I have created a WidgetControl adapter out of the widget DIVs and everything inside those divs. It encapsulates common operations like changing the title of the widget by clicking on the title bar, changing widget settings by clicking on “edit” link, deleting the widget by clicking on the close button, etc.

public class WidgetControl : Control<Div>
{
    public override global::WatiN.Core.Constraints.Constraint ElementConstraint
    {
        get
        {
            return Find.ByClass(className => className == 
			"widget" || className.Contains("widget "))
                .And(Find.ById("new_widget_template").Not());
        }
    }

    public string Title
    {
        get
        {
            return TitleLink.Text.Trim();
        }
    }

    public Link TitleLink
    {
        get
        {
            return base.Element.Link(Find.ByClass("widget_title_label"));
        }
    }

    public TextField TitleEditor
    {
        get
        {
            return base.Element.TextField(Find.ByClass("widget_title_input"));
        }
    }

    public Button TitleSaveButton
    {
        get
        {
            return base.Element.Button(Find.ByClass("widget_title_submit"));
        }
    }

    public Link CloseLink
    {
        get
        {
            return base.Element.Link(link => link.Id.EndsWith("CloseWidget"));
        }
    }

    public Link EditLink
    {
        get
        {
            return base.Element.Link(Find.ByClass("widget_edit"));
        }
    }

    public Div Header
    {
        get
        {
            return base.Element.Div(div => div.ClassName.Contains("widget_header"));
        }
    }

    public void EditTitle()
    {
        TitleLink.Click();
    }

    public void SetNewTitle(string newTitle)
    {
        TitleEditor.TypeText(newTitle);
        TitleSaveButton.Click();
    }

    public void Close()
    {
        CloseLink.Click();
    }        
} 

Just like Page adapters, you create a control adapter by inheriting from Control<TheElement>. You can wrap Div, Span, Table, Button, Link, etc., any control that WatiN supports. Then you need to override the ElementConstraint property to return a Constraint that identifies the control on the page. A Constraint can be a simple Find.ByClass or Find.ById. The constraint I have composed here is quite complex. It selects only the DIVs having class “widget” somewhere in the class attribute of the DIV and not having a specific ID.

Once you have control adapters, you can write tests to test the control’s behavior. For example, the following test will test the title editing behavior.

[Specification]
public void User_can_change_widget_title()
{
    var browser = default(Browser);
    var widgetId = default(string);
    var newTitle = Guid.NewGuid().ToString();

    "Given a user with a page having some widgets".Context(() =>
    {
        BrowserHelper.ClearCookies();
        browser = BrowserHelper.OpenNewBrowser(Urls.Homepage);
    });
    "When user changes title of a widget".Do(() =>
    {
        using (browser)
        {
            var page = browser.Page<HomePage>();
            var widget = page.Widgets.First();
            widgetId = widget.Element.Id;

            widget.EditTitle();
            widget.SetNewTitle(newTitle);

            Thread.Sleep(1000);
        }
    });
    "It should persist the new title on next visit".Assert(() =>
    {
        using (browser = BrowserHelper.OpenNewBrowser(Urls.Homepage))
        {
            var widget = browser.Control<WidgetControl>(widgetId);
            Assert.Equal(newTitle, widget.Title);
        }
    });
} 

Here it launches the browser, finds the first widget on the page and then changes the title of the widget. Then it closes the browser, opens again and confirms the changed title really got saved in database.

Here’s how WatiN does it:

TestControlAdapterOpt.gif

This way, you can populate form test boxes, select items in dropdowns, test if client side validation is kicking in or not and finally submit form and ensure the submission worked. With WatiN, the possibilities are limitless.

Testing Client Side Behaviors and jQuery Animations

When you delete a widget, it uses jQuery animation to remove the widget from the page as well as an async postback to notify server that a widget has been removed. This is a typical AJAX behavior. You want to keep the page responsive by giving immediate feedback on the UI while the background update process works. Such a behavior can be tested this way:

Specification]
public void User_can_delete_widget()
{
    var browser = default(Browser);
    var page = default(HomePage);
    var deletedWidgetId = default(string);

    "Given a user having some widgets on his page".Context(() =>
        {
            BrowserHelper.ClearCookies();
            browser = BrowserHelper.OpenNewBrowser(Urls.Homepage);
            browser.WaitForAsyncPostbackComplete(10000);                    
        });

    "When user deletes one of the widget".Do(() =>
        {
            page = browser.Page<HomePage>();
            var rssWidget = RssWidgetControl.GetTheFirstRssWidget(page);
            deletedWidgetId = rssWidget.Element.Id;
            rssWidget.Close();
            browser.WaitForAsyncPostbackComplete(10000);                    
        });

    "It should remove the widget from the page".Assert(() =>
        {
            using (browser)
            {
                Assert.False(browser.Element(deletedWidgetId).Exists);
            }
        });

    "It should not come on revisit".Assert(() =>
        {
            using (browser)
            {
                browser.Refresh();
                browser.WaitForAsyncPostbackComplete(10000);

                Assert.False(browser.Element(deletedWidgetId).Exists);
            }
        });
}

Here the code deletes the first RSS widget. Then it confirms the widget is really gone from the page. This validates if the jQuery stuff worked or not. Then it refreshes the browser and confirms if the widget is permanently gone or not.

Here’s how it works:

DeleteWidgetOpt.gif

Following the same approach, you can check if jquery validations are working or not. If you have jQuery grid, you can check if the grid loads data and allows editing or not. Almost any client side action can be simulated and tested this way.

Testing Drag & Drop using WatiN

This was the most difficult one. On Dropthings, you can drag & drop widgets from one place to another. In order to simulate this, I had to use JavaScript magic to raise mousedown, mousemove and mouseup events programmatically.

[Specification]
public void User_can_drag_widget()
{
    var browser = default(Browser);
    var page = default(HomePage);
    var widget = default(WidgetControl);
    var position = default(int[]);

    BrowserHelper.ClearCookies();

    "Given a new user on a page full of widgets".Context(() =>
        {
            browser = BrowserHelper.OpenNewBrowser(Urls.Homepage);
            browser.WaitForAsyncPostbackComplete(10000);
        });
    "When user drags a widget from first column".Do(() =>
        {
            page = browser.Page<HomePage>();
            widget = page.Widgets.First();
            position = browser.FindPosition(widget.Element);

            // Start the drag
            var mouseDownEvent = new NameValueCollection();
            mouseDownEvent.Add("button", "1");
            mouseDownEvent.Add("clientX", "0");
            mouseDownEvent.Add("clientY", "0");
            widget.Header.FireEventNoWait("onmousedown", mouseDownEvent);

            Thread.Sleep(500);

            // Move to the second column widget zone container
            var widgetZones = browser.Divs.Filter(Find.ByClass("widget_zone_container"));
            var aWidgetZone = widgetZones[0];
            var widthOfWidgetZone = int.Parse
				(aWidgetZone.GetAttributeValue("clientWidth"));

            for (var x = 0; x <= (widthOfWidgetZone * 1.5);  
			 x += (widthOfWidgetZone / 4))
            {
                var eventProperties = new NameValueCollection();
                eventProperties.Add("button", "1");
                eventProperties.Add("clientX", x.ToString());
                eventProperties.Add("clientY", "20");

                widget.Header.FireEventNoWait("onmousemove", eventProperties);
                Thread.Sleep(500);
            }
        });
    "It should move out of the column and be floating".Assert(() =>
        {
            using (browser)
            {
                var newPosition = browser.FindPosition(widget.Element);

                Assert.NotEqual(newPosition[0], position[0]);
                Assert.NotEqual(newPosition[1], position[1]);
            }
        });
} 

Here’s how it works:

DragDropTestOpt.gif

Here you can see the test first loads the website, then it simulates drag experience on a widget by holding the mouse down on the title bar and then moving it across the page. The test verifies that the widget did really move the position and thus confirms the jQuery plugin used for drag & drop really works.

However, one thing I could not find is how to test the drop of the widget. Although I move the widget on the second column, it does not detect that the widget has been moved to the second column and rearranges the widgets. The sortable plugin of jQuery is not getting the necessary events fired. If you can solve it, please do let me know.

Conclusion

Tests written using WatiN replace the need for human driven tests and thus shed significant time off your regular regression test suite. Moreover, it empowers developers with a way to quickly run regression tests whenever they need to, without waiting for human QA resource’s availability. When you hook it with xUnit like test frameworks and integrate with your continuous build, you can run UI tests automatically to test all the UI scenarios overnight after your nightly build and generate reports without requiring any manual intervention.

History

  • 6th August, 2010: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Omar Al Zabir
Architect BT, UK (ex British Telecom)
United Kingdom United Kingdom

Comments and Discussions

 
GeneralMy vote of 5 PinmemberSandyYu17-Oct-12 22:40 
GeneralMy vote of 5 Pinmemberthatraja6-Oct-10 21:48 
QuestionajaxStart/Complete Pinmemberstej_cz23-Aug-10 21:22 
AnswerRe: ajaxStart/Complete PinmvpOmar Al Zabir31-Aug-10 6:50 
GeneralMy vote of 5 PinmemberPranay Rana18-Aug-10 1:01 
GeneralMy vote of 5 PinmemberShahriar Iqbal Chowdhury15-Aug-10 0:26 
GeneralMy vote of 5 Pinmembermahmud6668-Aug-10 18:46 
GeneralMy vote of 5 Pinmemberlinuxjr7-Aug-10 12:47 
General5 From me PinmemberMoim Hossain6-Aug-10 10:38 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.1411023.1 | Last Updated 11 Jun 2011
Article Copyright 2010 by Omar Al Zabir
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid