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();
}
}