Tuesday, March 22, 2011

Watin on Crack

Here is an example of how we can create some simple and elegant Watin test code:
namespace Website.WebTests.Users
{
    public class AddNewUserTests : BaseTests
    {
        private static AddNewUserPage CurrentPage(IeBrowser browser)
        {
            return browser
                    .GoToPage<LoginPage>(page => page.Login())
                    .GoToPage<AdminPage>()
                    .GoToPage<AddNewUserPage>();            
        }
 
        [Test]
        public void AddNewUserPage_AddUser()
        {
            using (var browser = new IeBrowser())
            {
                var firstName = RandomString(15);
                var lastName = RandomNumber(10);
 
                browser
 
                // Arrange
                .GoToPage(CurrentPage)
 
                // Act
                .AddNewUser(new UserModel { FirstName = firstName, LastName = lastName })
 
                // Assert 
                .AssertPageHasText(page => page.Contains("Name: " + firstName));
            }      
        }              
    }
}

Easy to read, clean and maintainable...

Main points to note:
1) Each test includes a static method called CurrentPage, this has the steps required to navigate from the home page to the currently tested page, as per a user navigating to that page, including any actions that might need to occur along the way (eg page.Login)
2) Simple fluent interface for navigating pages and for setting up our Arrange, Act, Assert scenarios!

It may be a case of love it or hate it, however if you think as I do, that the above implementation is easy to read and understand then read on to see how it is done...

INTRODUCTION

As part of a project to create a new set of Watin tests for an existing Web Application, I decided to try to introduce some new functionality to achieve the following goals:

1) Speed up the development of the Watin web tests
2) Use the new Watin Page class to enable better reuse of code
3) Add extra helper methods to reduce issues with timing
4) Use a fluent interface to increase readability

In the past we were writing Watin tests as one long method with a bunch of actions in a row and then some kind of check or assert at the end. The problem with this approach is that there usually ended up being a lot of repeated code, for example when two tests have to navigate through the same page, then the various actions on the page could end up in two places.

The new (ok it's been around a couple of years) way to segregate your Watin code is to create a Watin Page class for each of your web pages. Thus you might have a login page, a search page, register page, confirmation page, etc. If all the pages include some common master page then those pages could derive from a base page with common elements. Each page would then have a bunch of properties, one property per UI element that you want to use, and then a number of actions that can apply to that page.

The beautiful part of the Watin Page class is that we simply create properties for each UI element, and add an attribute above each one specifying how we identify it on the page, for example IdRegex = "UserName", would look for an element which has Id="UserName". You could also do Id="UserName", and there are almost hundreds of variations. As you use the properties to retrieve or set data on the web page the Watin framework automatically finds the correct element using the [FindBy()] attribute, where you have passed in the method of identification.

Example of a LoginPage class:
namespace WebApplication.WebTests.Pages
{
    public class LoginPage : BasePage<LoginPage>
    {
        private const string UserName = "admin";
        private const string Password = "#test1";
 
        [FindBy(IdRegex = "UserName")]
        public TextField UsernameTextField;
 
        [FindBy(IdRegex = "Password")]
        public TextField PasswordTextField;
 
        [FindBy(ValueRegex = "Log on")]
        public Button LoginButton;
 
        /// <summary>
        /// Logs the user in
        /// </summary>
        /// <returns></returns>
        public LoginPage Login()
        {
            UsernameTextField.Value = UserName;
            PasswordTextField.Value = Password;
 
            LoginButton.Click();
 
            return this;
        }
    }
}

The above page class encapsulates the logic that is specific to our Login page. Namely three UI elements, the username and password text fields and the login button. I have also included an action that is applicable to this page, ie logging in, and if one were to call the Login method, then the Watin test would log us in.

Each page in our web site has a matching page, similar to the above one, and I structure them in our WebTests project using a similar folder hierarchy as per the websites folder hierarchy.

The first thing to note about these Page class implementations is that they all derive from BasePage<T>. This allows us to reuse some common functionality across all pages, for example the navigation buttons along the top which occur on all pages, the log off link that we have on all pages, and a number of useful Assert helper methods, some useful 'Wait' methods to deal with timing issues and some methods that start with 'GoTo' which we use for navigation (to be explained later).

The second thing to note about the methods in these Page classes, is that all methods return a reference (return this) to themselves. This is how we enable our fluent interface, each page action can be appended on to the last page action, and in the base class there are actions that allow us to navigate between pages as well so the entire Watin test could be one long set of chained methods!

THE BASE PAGE : BasePage<T>

The idea behind having a generic base class is that even the BasePage actions can return the Type of the derived class. If the BasePage wasn't generic, then we would need to specify which Type to return in our fluent base methods, either that or just return a reference to the BasePage, which would mean that in the test itself we would need to do a cast or else miss out on being able to access derived class methods.

Being able to define base class methods like this:
        public T Logoff()
        {
            LogoffLink.Click();
            return (T)this;
        }    
Means that every derived BasePage class automatically has a fluent method that does a Logoff and also returns a reference to the derived class, automatically allowing us to chain the next derived fluent method without having to interrupt our fluency to add some unsightly casts. 

THE BROWSER CLASS

Watin tests all start with the creation of a browser instance. For IE, which is what I will be using, the class is also called IE. I then derive a class from this, called the IeBrowser class. This will be our starting point for tests. Instantiating this IeBrowser class automatically opens a copy of IE on your desktop. The IeBrowser class has a number of GoTo methods that get our navigation started.

Here is the constructor:
        public IeBrowser()
        {
            AutoClose = true;
            Settings.AutoMoveMousePointerToTopLeft = false;     
            
            ClearCache();
            ClearCookies();
        }

Here are our Browser GoTo navigation methods:

        public T GoToPage<T>() where T : BasePage<T>, new()
        {
            GoTo(BaseUrl);
 
            return Page<HomePage>().GoToPage<T>();
        }

The first method is our initial GoToPage method. It's a generic method so we just pass the Type of the page we want to navigate to. It starts by taking us to an absolute URL (the only hard coded url within the project) which is the home page of the website. Then the page that you have specified needs to be one that is accessible from the HomePage, this ensures that the test follows the normal flow of user navigation.

        public T GoToPage<T>(Func<IeBrowser, T> stepsToGetToThisPage) where T : BasePage<T>, new()
        {
            var page = stepsToGetToThisPage(this);
 
            page.WaitUntilComplete();
 
            return page;
        }  

In addition to the first GoToPage method, we have a second more elaborate version which allows us to pass an anonymous function returning the page type T, and accepting an IeBrowser instance. This anonymous function then can contain a chain of GoToPages, ie to quickly allow us to navigate to a certain page. Each page also can also contain some custom WaitUntilComplete functionality so that we only return from the method when we know that the page has finished loading.

BASE PAGE GOTO METHODS

The BasePage class also contains similar GoToPage methods, and here is where the real magic takes place. To keep things as simple as possible, when we create our new derived Page classes, we check to see what navigation links are present. For example on our HomePage I have:

    public abstract class BasePage<T> : Page where T : BasePage<T>
    {          
        [LinkedPage(typeof(RegisterPage))]
        [FindBy(IdRegex = "RegisterLink")]
        public Link RegisterLink;
 
        [LinkedPage(typeof(ResultsPage))]
        [FindBy(IdRegex = "ResultsLink")]
        public Link ResultsLink;
 
        [LinkedPage(typeof(AdminPage))]
        [FindBy(IdRegex = "AdminLink")]
        public Link AdminLink;
 
        [LinkedPage(typeof(LoginPage))]
        [FindBy(Id = "logonLink")]
        public Link LogonLink;    

The Page starts with our Watin user UI elements as per normal, but any UI elements that are actually navigation links include a new LinkedPage attribute. This signifies to our framework that the particular link is also a conduit that allows us to navigate to the specified Page type.  The above links are common across all pages, that is why they are in the base class, however our derived page classes will also contain similar links and LinkedPage attributes where necessary.

The BasePage GoToPage method looks like this:

        /// <summary>
        /// Goes to page of the specified type by clicking on the  matching link and then returning the correct page type
        /// </summary>
        /// <typeparam name="TPage"></typeparam>
        /// <returns></returns>
        public virtual TPage GoToPage<TPage>() where TPage : BasePage<TPage>, new()
        {
            // click on the link which will take us to matching page (see LinkedPage attribute on Link properties above)
            LinkedPageAttribute.GetLinkedElement<TPage, Link>(this).Click();
 
            // create required page
            var basePage = Document.Page<TPage>();
 
            // allows us to add some custom wait functionality to delay processing until we are happy that page has loaded
            basePage.WaitUntilComplete();
 
            return basePage;
        }

Thus on the current page, when we call GoToPage<RegisterPage> for example, the call to GetLinkedElement will look for a type 'Link' which is of type 'TPage' and call the Click method on it. Note we could pass in other Types, eg Button if we so wish. The Page is then created using the Watin Document.Page<T> method and we wait for the page to complete. These GoToPages are then chained together and as long as each page has links to the next page the whole set of chained actions will complete, and to the user watching the test in action it will just appear as if a user was navigating through the site.

I have also added another version of the above GoToPage method which also allows us to pass an action to perform on that page:

        /// <summary>
        /// Version of GoToPage as per above, which calls GoToPage, but which also takes an action to perform on that page
        /// as a parameter and then performs that action on the page
        /// </summary>
        public virtual TPage GoToPage<TPage>(Func<TPage, TPage> actionToCall) where TPage : BasePage<TPage>, new()
        {
            // go to the required page as per usual
            var page = GoToPage<TPage>();
 
            // call the required action on the page
            return actionToCall(page);
        }

BASE PAGE TIMING METHODS

Timing is a big issue with web testing. The issue of course, is that each web page can take a different time to render, and different parts of each page can render differently, some with the page load, some using ajax etc. Therefore I have added a virtual method that can be overridden in each Page derivation so that we can specific custom Wait functionality. Here is the method in the BasePage:

        /// <summary>
        /// Override this method if you have a specific element or text you want to
        /// wait to be loaded before continuing with processing
        /// </summary>
        public virtual T WaitUntilComplete()
        {
            return (T)this;            
        }

For example a derived page may display a data grid and then a button at the end to add a new item. The button is only displayed after the grid is drawn and this could take several seconds. We don't want to operate on the page until the grid is complete, so we can put a check on whether the add button exists, eg:

        /// <summary>
        /// Wait until the add button is displayed before continuing
        /// any tests that use this page.
        /// </summary>
        public override GridPage WaitUntilComplete()
        {
            AddButton.WaitUntilExists();
 
            return this;
        }

There is also a useful BasePage generic extension method for waiting for text:

        public static T WaitForText<T>(this BasePage page, string text) where T : BasePage, new()
        {
            page.Document.WaitUntilContainsText(text);
 
            return (T)page;
        } 

BASE PAGE ASSERT HELPER METHODS

To be able to perform fluent asserts, I have added a number of useful Assert helper methods to our BasePage class.

Here is a quick overview:

1) Check to see that the page contains some text. The WaitUntilContainsText makes sure that we rather time out waiting for the text than looking for the text too early. The end result is no timing issues.


        public T AssertPageHasText(string text)
        {
            Document.WaitUntilContainsText(text);
            Assert.IsTrue(Document.Text.Contains(text));
            return (T)this;
        }

2) Assert text exists by using our own custom logic.

        public T AssertPageHasText(Func<string,bool> doesPageContainText)
        {
            Document.WaitUntil(() => doesPageContainText(Document.Text));
            Assert.IsTrue(doesPageContainText(Document.Text));
            return (T)this;
        }

Usage example for above:

        // Assert (can either be a successful addition / or if Id is already used then we get a validation message) 
        .AssertPageHasText(page => page.Contains("Summary: " + fullName) || page.Contains("The Id is already in use"));

3) The next lot of asserts are self explanatory

          public T AssertPageMissingText(string text)
        {
            Assert.IsFalse(Document.Text.Contains(text));
            return (T)this;
        }
 
        public T AssertPageHasLink(string text)
        {
            Assert.IsNotNull(Document.Link(link => link.Text.Contains(text)));
            return (T)this;
        }
 
        public T AssertPageMissingLink(string text)
        {
            Assert.IsTrue(Document.Links.Filter(link => link.Text.Contains(text)).Count == 0);
            return (T)this;
        }
 
        public T AssertTextFieldHasText(string text)
        {
            Assert.IsNotNull(Document.TextField(textfield => textfield.Text.Contains(text)));
            return (T)this;
        }

4) Assert CatchAll, for anything else that I haven't thought of, we can use this method. We can essentially
put any assert or other anonymous method that returns a bool.
        /// <summary>
        /// Allows us to wrap any assert statement into our fluent interface
        /// </summary>
        public T AssertThat(Action assert)
        {
            assert();
            return (T)this;
        }
Usage of this assert would be something like this:
        .AssertThat(() => Assert.AreEqual(newYear, currentYear));

TEST CLASSES

The test classes are also kept in a similar folder hierarchy as per the pages, but under the heading Tests.

Each test page has a static method called CurrentPage, this has the sequence of navigation steps required to get to the current page. Then each test that follows essentially calls the GoToPage method on the IeBrowser class passing in that function. That has the effect of navigating us to the desired page, whereupon we can do the Act and Assert parts of the test. The idea of having this CurrentPage method is that it can be reused by all the test methods within the Test class. Each one will re-navigate from the home page back to the current page.

There will usually be a number of test methods in the test page class. For example one to see the page exists and that navigation to the page works. Then one to add an item if possible, one to search for example, one to delete if possible, others as necessary, etc.

Here is a random example of a test page:

namespace Website.WebTests.Admin
{
    public class UserLevelTests : BaseTests
    {
        private static UserLevelsPage CurrentPage(IeBrowser browser)
        {
            return browser
                      .GoToPage<LoginPage>(page => page.Login())
                      .GoToPage<AdminPage>()
                      .GoToPage<UserLevelsPage>();
        }
 
        [Test]
        public void UserLevels_Exist()
        {
            using (var browser = new IeBrowser())
            {
                browser
                    
                // Arrange
                .GoToPage(CurrentPage)
 
                // Assert
                .GetResultsTable()
                .AssertTableHasRows();
            }
        }
 
        [Test]
        public void UserLevels_AddNewValidLevel_CanBeAdded()
        {
            using (var browser = new IeBrowser())
            {
                var userLevel = Guid.NewGuid().ToString();
 
                browser
 
                // Arrange
                .GoToPage(CurrentPage)
 
                // Act
                .AddUserLevel(new UserLevel { UserLevelName = userLevel, Comments = "Comments", IsActive = true })
 
                // Assert
                .AssertPageHasText(userLevel);
            }
        }
 
        [Test]
        public void UserLevels_CanBeEdited()
        {
            using (var browser = new IeBrowser())
            {
                var comments = Guid.NewGuid().ToString();
 
                browser
                    
                // Arrange
                .GoToPage(CurrentPage)
                   
                // Act
                .EditUserLevelComment(1, comments)
 
                // Assert
                .AssertPageHasText(comments);
            }
        }
    }
}

To get an idea of what the action is doing on the UserLevelsPage, here is the AddUserLevel action:

        public UserLevelsPage AddUserLevel(UserLevel userLevel)
        {
            AddUserLevelButton.Click();
 
            LevelNameTextField.Value = userLevel.UserLevelName;
            LevelCommentsTextField.Value = userLevel.Comments;
            ActiveCheckBox.Checked = userLevel.IsActive;
 
            AddUserLevelOkButton.Click();
 
            return this;
        }

I tend to reuse existing domain entities or view classes for passing data into the Page classes.

Sometime it is also useful to pull out information from the page class so we can use this data in our Asserts, for example:

        [Test]
        public void UserDetails_RemoveUserFee_TotalSpentDecreases()
        {
            using (var browser = new IeBrowser())
            {
                decimal oldUserFeesSpent, newUserFeesSpent, feeAmount;
 
                browser
 
                // Arrange
                .GoToPage(CurrentPage)
                .GetUserFeesSpent(out oldUserFeesSpent)
 
                // Act
                    .RemoveLastUserFee(out feeAmount)
               .GetUserFeesSpent(out newUserFeesSpent)
 
                // Assert
                    .AssertThat(() => Assert.AreEqual(oldUserFeesSpent - feeAmount, newUserFeesSpent));
            }
        }  

To keep the fluent interface intact I had to use 'out's to return information retrieved from the page. This way this information can be acted upon within the test, for example in the Assert.

A more elaborate example with more steps:

        [Test]
        public void AddNewUser_AdminUser()
        {
            using (var browser = new IeBrowser())
            {
                browser
 
                // Arrange
                .GoToPage(CurrentPage)
 
                // Act
                .SelectApplication(UserTypeEnum.Admin)
                .EnterCaptcha()
                .AcceptTermsAndConditions()
                .AddPersonalDetails(AdminApplication)
                .AddAddressDetails(AdminApplication)
                .Next(2)
                .AddObjective(AdminApplication)
                .AddAttachment(FileToUploadPath)
                .SubmitApplication()
 
                // Assert
                .AssertPageHasText("User application complete");
            }
        }

I haven't quite managed to integrate a test which pops up a new browser window which also needs to be included in the test, this is the best I can do:

        [Test]
        public void UserBiographyPage_DefaultSearch_ReturnsResults()
        {
            using (var browser = new IeBrowser())
            {
                browser
 
                // Arrange
                .GoToPage(CurrentPage)
 
                // Act
                .SearchUsers(new UserRequest { CategoryIds = new List<int> { 2 }, YearFrom = 2010, YearTo = 2011 });
 
                // Assert
                using (var poppedUpBrowser = browser.GetPopupBrowser("Website/Results/ViewUserReport"))
                {
                    poppedUpBrowser.Page<UserDetailsPage>()
 
                    .WaitUntilComplete()
                    .GetResultsTable()        
                    .AssertTableHasRows();
                }
            }
        } 
One last point that was kindly raised by a reader was a concern about having to re-navigate to the CurrentPage for each test in the module. Sometimes this is unnecessary and if you know that it is ok to run your tests one after the other on the same page without causing undue effects then we can gain a time advantage by not having to re-navigate, ie calling the GoToPage method in the Test.

The way I suggest to handle this situation is to create the Browser instance and then do the GoToPage navigation in the Setup method for the test module. This way it happens once, and each test then skips the GoToPage and instead we would call a new method (yet to be added) maybe called GetPage<T> where we can return the page of that type and start working directly on that.

SOME OTHER FUNKY STUFF

Sometimes you can have a wierd situation where you have to operate on a UI element which hasn't yet been created. This can happen for example when some AJAX on some condition then reveals a new TextField or some other such control which we then need to populate.

Therefore I have created a generic extension method to the Watin Document class which takes a Func (with some wait logic) and returns the specified type. This extension is called 'WaitUntilExists', here is an example of its use:

            AddEmailButton.Click();
 
          Func<TextField> getNewEmailTextField = () =>
          {   var row = EmailsTable.GetLastRow();
              var textField = row.TextFields.FirstThatIsNotHidden();
              return row.TextFields.FirstThatIsNotHidden().Value.IsNullOrEmpty() ? textField : null;
          };
 
          // do above step until we get text field back
             var emailAddressField = Document.WaitUntilExists(getNewEmailTextField);
 
            // populate last email address field
            emailAddressField.Value = emailDetails.EmailAddress;

Clicking the AddEmailButton button will pop in a new row into our list of email addresses. The WaitUntilExists keeps polling the Func<T> that is passed in, until it returns a valid T (rather than null), which happens when it exists and then returns this.

The extension method looks like this:

        /// <summary>
        /// Useful extension method that returns an element that you need in the case when the element
        /// might take some time to render due to AJAX etc. Simply pass in a method that returns that
        /// variable or null if it doesn't exist. This generic extension method than invokes the function
        /// until it gets a non null element out which it passes back. We also put a timeout in so we
        /// don't end up hanging if the element is never found. 100 attempts = 20 seconds.
        /// </summary>
        public static T WaitUntilExists<T>(this Document document, Func<T> existsCheck) where T : Element
        {
            var attempts = 100;  
            T element;
 
            while ((element = existsCheck()) == null)
            {
                Thread.Sleep(200);
 
                if (--attempts == 0) break;
            }
 
            return element;
        }

There is also another extension method for more simple situations where we just need to wait for a True check, eg:

            .....
 
            SaveButton.Click();
 
            // wait until save button is re-enabled again (means save has gone through)
            Document.WaitUntil(() => SaveButton.Enabled);
 
            return this;
        }

Where the extension method is:

        /// <summary>
        /// Waits the until the specified func returns true.
        /// </summary>
        public static void WaitUntil(this Document document, Func<bool> trueCheck)
        {
            var attempts = 100;
 
            while (trueCheck() == false)
            {
                Thread.Sleep(200);
 
                if (--attempts == 0) break;
            }
        }

LINKEDATTRIBUTE FUNCTIONALITY               

This is the attribute which allows us to specify which page a given UI element will navigate to. Simply pass in the Type of the Page that the specified element will go to when clicked. Our code for this attribute class looks something like this:

namespace Website.WebTests.Attributes
{
    public class LinkedPageAttribute : Attribute
    {
        public Type PageType { getset; }
 
        public LinkedPageAttribute(Type pageType)
        {
            PageType = pageType;
        }
 
        /// <summary>
        /// Gets the element of the specified type on the page passed in, which matches
        /// the provided page type, based on the LinkedPage attribute that is applied to the element.
        /// </summary>
        public static TElement GetLinkedElement<TPage,TElement>(Page page) where TElement : class
        {
            var fieldInfos = page.GetType().GetFields();
 
            foreach (var fieldInfo in fieldInfos)
            {
                var attributes = fieldInfo.GetCustomAttributes(typeof(LinkedPageAttribute), true);
                if (attributes.Length == 0) continue;
 
                var linkedPageAttribute = (from a in attributes where a.GetType() == typeof(LinkedPageAttribute) select a).FirstOrDefault();
                if (linkedPageAttribute == null || !(linkedPageAttribute is LinkedPageAttribute)) continue;
 
                if((linkedPageAttribute as LinkedPageAttribute).PageType == typeof(TPage))
                {
                    return fieldInfo.GetValue(page) as TElement;
                }
            }
 
            // you are trying to navigate to a page which cannot be reached from the current page, check to see you have a link with LinkedPage attribute
            throw new ArgumentException("You don't have a link to this page {0} from this page {1}".FormatWith(typeof (TPage), page.GetType()));
        }
    }
}

The meat of this class is in GetLinkedElement. Here we iterate over all the properties in the Page looking for any that are decorated with the LinkedPage attribute. We find the on the for the given page type and return that as a TElement type for clicking on...

Hopefully this has been useful and/or interesting, please post any comments or criticisms below :-)

8 comments:

  1. This looks absolutely rock solid. Thanks Frank.

    Any chance of a download of some code samples :)

    ReplyDelete
  2. This blog post rocks. A download link with the code samples would be great.

    ReplyDelete
  3. public T GoToPage(Func stepsToGetToThisPage) where T : BasePage, new()
    {
    var page = stepsToGetToThisPage(this);

    page.WaitUntilComplete();

    return page;
    }
    ////////////
    Is it not an infinite recursion? How does it work?

    ReplyDelete
  4. No there is no recursion in this method. The method passed in via the Func has the steps to get to the page we want to test. This GoToPage is in the IeBrowser class but there is also a GoToPage in the BasePage class, this may cause a bit of confusion. The one in the IeBrowser class kicks it off, and then we just jump from page to page using the GoToPage of the BasePage (and navigating via the LinkedPages) as defined at the top of the Tests class in the CurrentPage method - which is the one passed to the initial IeBrowser GoToPage method.

    ReplyDelete
  5. First of all excellent article. It has become my go-to article when trying to get up to speed with Watin, and how to write clean coded ui tests.

    However, I'm trying to implement this pattern for the Watin Unit tests I'm writing for an application, and the UI is designed differently than this example, and every other example that I've seen. The login page I'm referencing is separate from all the other pages, ie, you can't access the site without logging in. It's presenting problems in my BasePage class in regard to linkedpage attributes, because it is a separate entity all-together from the application, but the watin tests depend on it occurring before any other test can be ran. What would be the best way to implement this? Add a new method to the BasePage class that doesn't require any linkedpage attributes? Any recommendation in particular you could give would be helpful. Thanks in advance, and thanks again for the elegant code!

    ReplyDelete
  6. ie, When I structure my code like yours using an anonymous function:

    .GoToPage(page => page.Login())

    I get a build error:

    'Project.IeBrowser' does not contain a definition for 'Login' and no extension method 'Login' accepting a first argument of type 'Project.IeBrowser' could be found (are you missing a using directive or an assembly reference?)

    Although I have the same code as you for IeBrowser and BasePage classes.

    ReplyDelete
  7. Hi Brad, in one of my base classes I set the start home page url. It might be in your case you need to set the login page url first, do the login, and then redirect the test to the page you want to test. This might be necessary if you don't have a login page link available on your home page like I did.

    Regarding your build error, if you are using the same code as mine you will need to tell the GoToPage (which is a generic method) which page you want it to go to, ie .GoToPage(...

    At some point I will extract my code out and make it available for download!

    ReplyDelete
  8. This is excellent work. Thanks a lot! Could you please publish your work on Github so that we can access it and enhance our framework.

    ReplyDelete