Sunday, April 19, 2015

CodedUI - Attribute based page objects and Jquery selectors to find controls

In UI automation Page Object is a popular design pattern which helps you create UI tests that are easy to maintain and help reducing code duplication. The basic idea behind page objects is that you can create a page class that represents the complete HTML page under test in an object oriented way. For e.g. the controls on the HTML page will be represented by properties in the page class and actions as methods. By doing this it’s very easy to use the benefits of OOPS to create composable menus, headers, footers etc. to reuse those parts in the page objects. In short Page objects encapsulate the behaviors of the HTML page by exposing methods that reflects the user actions and hides the details of telling the browser how to do these things.
Attributes in C# provides a powerful method of associating declarative information in code (types, methods, properties, and so forth). Once associated with a program entity, the attribute can be queried at run time and used in many ways. We'll see how to use attributes in Page objects to provide information on how to find a control on the DOM and later use this in the UI tests. Also we'll see how to use the metadata from the attributes to create JQuery selectors and execute them on the page, using the BrowserWindow.ExecuteScript method to find controls on the page much faster.

Step 1: Creating custom attributes.

Page attribute:

The page attribute is used to provide information related to the page under test, for e.g we’ll use this attribute to provide details like the page url, browser information etc. A simple page attribute looks like.
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class PageAttribute : Attribute
{
    public PageAttribute(string url)
    {
        Url = url;
    }

    public PageAttribute()
    {

    }

    [Required(AllowEmptyStrings = false, ErrorMessage = "Url is a required property and has to be provided")]
    public string Url { get; set; }
}

Control attribute:

Control attributes are used to provide information related to the ui controls on the page, like the id of the control, class, jquery selectors etc.
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class ControlAttribute : Attribute
{
    public string Id { get; set; }

    public string Class { get; set; }

    public string Selector { get; set; }

    public PropertyExpressionOperator IdConditionOperator { get; set; }

    public PropertyExpressionOperator ClassConditionOperator { get; set; }
}

Other attributes

Similarly you can define other custom attributes like a SearchAttribute or a LazyLoad attribute that can be used to find controls based on custom data attributes or to denote that the controls are loaded at a later point of time etc.
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class SelectorAttribute : Attribute
{
    public SelectorAttribute(string value)
    {
        Value = value;
    }

    [Required(AllowEmptyStrings = false, ErrorMessage = "Value is a required property")]
    public string Value { get; set; }
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class LazyLoadedAttribute : Attribute
{
       
}

Step 2: Creating the Jquery builder class

The JQueryBuilder class is responsible for building the jquery selector based on the properties defined on the control. For e.g the builder will combine multiple attributes like id, class, tag etc. to a valid jquery selector that can be later used to identify the control on the DOM.
A simple jquery builder can be created as
public class JqueryBuilder
{
    public string Selector { get; private set; }

    public void WithId(string id, PropertyExpressionOperator @operator = PropertyExpressionOperator.EqualTo)
    {
        if (String.IsNullOrEmpty(id))
        {
            return;
        }
        if (@operator == PropertyExpressionOperator.EqualTo)
        {
            Selector = String.Concat(Selector, "#", id);
        }
        else
        {
            SearchBy("id*", id);
        }
    }

    public void WithClass(string name, PropertyExpressionOperator @operator = PropertyExpressionOperator.EqualTo)
    {
        if (String.IsNullOrEmpty(name))
        {
            return;
        }
        if (@operator == PropertyExpressionOperator.EqualTo)
        {
            Selector = String.Concat(Selector, ".", name);
        }
        else
        {
            SearchBy("class*", name);
        }
    }

    public void WithSelector(string selector)
    {
        if (String.IsNullOrEmpty(selector))
        {
            return;
        }
        Selector = selector;
    }

    private void SearchBy(string name, string value)
    {
        Selector = String.Concat(Selector, "[", name, "=", value, "]");
    }      
}

Step 3: Extending the BrowserWindow object to use jquery selectors

Use the ExectuteScript method on the BrowserWindow its now easy to find controls on the page by injecting javascript into the page. In this example we’ll be using jquery to find controls on the page using selectors. But before that you should ensure that jquery is available on the page before using the selectors. The EnsureJqueryIsAvailable method will ensure that jquery is available on the page under test.
private static void EnsureJqueryIsAvailable(this BrowserWindow window)
{
    var needsJquery = (bool) window.ExecuteScript("return (typeof $ === 'undefined')");
    if (needsJquery)
    {
        window.ExecuteScript(@"
            var scheme =  window.location.protocol;
            if(scheme != 'https:')
                scheme = 'http:';
            var script = document.createElement('script');
            script.type = 'text/javascript';
            script.src = scheme + '//code.jquery.com/jquery-latest.min.js';
            document.getElementsByTagName('head')[0].appendChild(script);
        ");
    }
}
Next we’ll define two extension methods on the BrowserWindow to find controls using a jquery selector. The Generic find method is used to typecast the control to a generic control.
public static HtmlControl FindBySelector(this BrowserWindow window, string selector)
{
    window.EnsureJqueryIsAvailable();
    var uiControl = window.ExecuteScript(string.Format("return $('{0}')", selector));
    if (uiControl == null)
        return null;

    var hasMultipleResults = uiControl.GetType() == typeof (List<object>);
    if (hasMultipleResults)
    {
        return ((List<object>) (uiControl)).First() as HtmlControl;
    }
    return uiControl as HtmlControl;
}

public static T FindBySelector(this BrowserWindow window, string selector) where T : HtmlControl, new()
{
    window.EnsureJqueryIsAvailable();
    var control = new T {Container = window};
    var tag = control.TagName;
    if (!selector.ToUpper().StartsWith(tag.ToUpper()))
    {
        selector = String.Concat(tag, selector);
    }

    var uiControl = window.ExecuteScript(string.Format("return $('{0}')", selector));
    if (uiControl == null)
        return null;

    var hasMultipleResults = uiControl.GetType() == typeof (List<object>);
    if (hasMultipleResults)
    {
        var collection = (List<object>) uiControl;
        if (collection.Any())
        {
            return collection.First() as T;
        }
    }   
    return uiControl as T;
}

Step 4 : Creating the base page object

The base page object will implement the methods to load a page, parse the attributes and load controls on the page that can be later used to perform tests. The base page object defines the static creation method Create, that is responsible for creating a new instance of the page object and populating the properties with controls on the DOM.
public static TPage Create() where TPage : HtmlPage, new()
{
    var page = new TPage();
    var url = LoadAttributePageAttribute>().Url;
    page.Open(url);
    page.InitializeControls();
    return page;
}
The Open method launches a new browser window based on the url property on the page. The initialiaze controls methods is responsible for control finding and populating the page with the controls on DOM.
The complete base page looks like.
public class HtmlPage
{
    internal BrowserWindow Window { get; private set; }
    public void Open(string url)
    {
        var uri = new Uri(url);
        InitilizePlayback();
        Window = BrowserWindow.Launch(uri);
        Window.Maximized = true;
    }

    internal HtmlControl Find(string selector)
    {
        return Window.FindBySelector(selector);
    }

    internal T Find(string selector) where T : HtmlControl, new()
    {
        return Window.FindBySelector(selector);
    }

    private void InitilizePlayback()
    {
        if (!Playback.IsInitialized)
        {
            Playback.Initialize();
        }
        if (!Playback.IsSessionStarted)
        {
            Playback.StartSession();
        }
        SetSearchSettings(Playback.PlaybackSettings);
        Playback.PlaybackSettings.ContinueOnError = true;
        Playback.PlaybackError += (sender, args) =>
        {
            if (args.Result == PlaybackErrorOptions.Skip) return;
            if (Window != null)
            {
                Window.Close();
            }
        };
    }

    static void SetSearchSettings(PlaybackSettings settings)
    {
        settings.SearchInMinimizedWindows = true;
        settings.SearchTimeout = 10000; //10 seconds
        settings.ShouldSearchFailFast = true;
        settings.WaitForReadyLevel = WaitForReadyLevel.UIThreadOnly;
        settings.WaitForReadyTimeout = 5000;
    }

    internal void InitializeControls(bool lazyLoaded = false)
    {
        var map = new PropertyInfoToControlMap();

        //Find all properties on page with the control attribute
        var controlProperties = GetType().GetProperties()
            .Where(prop => prop.IsDefined(typeof(ControlAttribute), false)).ToList();

        var controls = map.From(controlProperties).Where(c => c.IsLazyLoaded == lazyLoaded);
        foreach (var control in controls)
        {
            var property = controlProperties.FirstOrDefault(c => c.Name == control.Name);
            MethodInfo findMethodInfo;
            if (property.PropertyType != typeof (HtmlControl))
            {
                findMethodInfo =
                    typeof (HtmlPage).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
                        .First(m => m.Name == "Find" && m.IsGenericMethod);
                findMethodInfo = findMethodInfo.MakeGenericMethod(property.PropertyType);
            }
            else
            {
                findMethodInfo =
                    typeof(HtmlPage).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
                        .First(m => m.Name == "Find");
            }
               
            var htmlControl = findMethodInfo.Invoke(this, new object[] { control.Selector });
            if (htmlControl == null && !control.IsLazyLoaded)
                throw new ArgumentException(String.Format("Page control {0} does not exist", control.Name));
            if (!htmlControl.GetType().IsAssignableFrom(property.PropertyType)) continue;
            property.SetValue(this, htmlControl);
        }
    }

    public void Refresh()
    {
        InitializeControls(true);
    }

    public static TPage Create() where TPage : HtmlPage, new()
    {
        var page = new TPage();
        var url = LoadAttributePageAttribute>().Url;
        page.Open(url);
        page.InitializeControls();
        return page;
    }

    private static T LoadAttribute()
        where TPage : HtmlPage
        where T : Attribute
    {
        var attribute = Attribute.GetCustomAttribute(typeof(TPage), typeof(T));           
        return attribute as T;
    }
}

Step 5: Translating the attributes to a control object.

The PropertyInfoToControlMap  object is responsible to create a control object with the selector property by parsing the attributes and building a selector using the JqueryBuilder class.
internal class Control
{
    public Control(string name)
    {
        Name = name;
    }

    public string Name { get; set; }
    public string Selector { get; set; }
    public bool IsLazyLoaded { get; set; }
}
internal class PropertyInfoToControlMap : IMap<Control, PropertyInfo>
{
    public IList<Control> From(IList<PropertyInfo> source)
    {
        return source.Select(ToControl).ToList();
    }

    private Control ToControl(PropertyInfo property)
    {
        var control = new Control(property.Name);
        var controlAttribute = property.GetCustomAttribute<ControlAttribute>(false);
        var builder = new JqueryBuilder();
        builder.WithId(controlAttribute.Id);
        builder.WithClass(controlAttribute.Class);
       
        var selectorAttribute = property.GetCustomAttribute<SelectorAttribute>(false);
        if (selectorAttribute != null)
        {
            builder.WithSelector(selectorAttribute.Value);
        }
        control.Selector = builder.Selector;
        control.IsLazyLoaded = property.GetCustomAttribute<LazyLoadedAttribute>(false) != null;
        return control;
    }
}

Step 6: Creating your page object.

Now you are ready to create your page objects using the objects created in the previous steps. Let’s see how a sample page object looks like
[Page(Url = "http://www.blogsprajeesh.blogspot.com/")]
public class BloggerPage : HtmlPage
{
    [Control]
    [Selector("h1 .title")]
    public HtmlControl Title { get; set; }

    [Control(Id = "Image2_img")]
    public HtmlImage ProfilePicture { get; set;  }
       
    public void EnsureTitleOnPage()
    {
        Assert.IsNotNull(Title);
    }

    public LinkedInPage NavigateToLinkedInPage()
    {
        Mouse.Click(ProfilePicture);
        Refresh();
        return new LinkedInPage();
    }

    public BloggerPage EnsureProfilePictureOnPage()
    {
        Assert.IsNotNull(ProfilePicture);
        return this;
    }
}

Step 7: Creating the tests

Using the page objects created, you can now write the tests like.
[CodedUITest]
public class BloggerPageTests
{
    [TestMethod]
    public void BlogHome_ShouldHaveATitleOnPage()
    {
        HtmlPage.Create<BloggerPage>()
            .EnsureTitleOnPage();
    }

    [TestMethod]
    public void BlogProfile_ShouldTakeToLinkedInPage()
    {
        HtmlPage.Create<BloggerPage>()
            .EnsureProfilePictureOnPage()
            .NavigateToLinkedInPage();
    }
}

No comments: