In this blog post, I will describe the various key points that need to be addressed while implementing a new control support in a Coded UI Test Plugin (a  WPF plugin in this case). A sample WPF Calendar control example gives an in-depth on the various implementation details.

 

I assume that the reader has an overall knowledge of UITest extensibility framework. Else to begin with, you can have a look at the series posted here.

 

 

RECORDING

 

I.                    Actions to Record

 

The foremost decision to make is to decide on the type of actions (UITestAction) to be recorded on the control.

 

Snapshot below shows a WPF Calendar control and its corresponding accessibility tree (UI Automation tree structure). A click on the day button selects the date in the Calendar. User can navigate to a month by click on the buttons in the header (“Previous button”, “Next button” or the month button, “December, 2009” in the snapshot below). 

 

 WPF Calendar UI hierarchy

 

Instead of recording individual clicks on the buttons, we will attempt to record SetValue action on Calendar. Primary reasons being –

a.       SetValue is more in line with intent-aware recording as compared to recording raw actions i.e. the recorder makes a best effort to capture the final intent of the user using minimal set of actions.

b.       During playback, the Calendar control need not be in the same state as during recording. For example, user should be able to playback a SetValue “16-Dec-2009” irrespective of the current month being displayed.

c.       For multi-selection of dates there will be one recorded action, something like SetValue (“16-Dec-2009”,”20-Jan-2009”) instead of recording multiple raw actions. So we will minimize the number of recorder actions.

 

 

II.                  Event Handler and Notification

 

When user clicks on a day button, the recorder’s low level hook thread pushes an action “MouseButtonClick on DayButton X” into the action stack.

 

It then invokes IUITechnologyManager: bool AddEventHandler(IUITechnologyElement element, UITestEventType eventType, IUITestEventNotify eventSink);

This API gives the plugin a chance to attach its own set of event listeners to the element, and notify back to the eventSink when an event is fired. Currently, the UITestEventType passed to event handler (from the recorder) is as follows:

 

UITestEventType.StateChanged à CheckBox, RadioButton, TreeItem, CheckBoxTreeItem

UITestEventType.ValueChanged à Rest of the controls.

 

For the WPF Calendar, we intend to notify back a ValueChanged event.

 

UI Automation (UIA) fires SelectionItemPattern.ElementAddedToSelectionEvent, SelectionItemPattern.ElementRemovedFromSelectionEvent and SelectionItemPattern.ElementSelectedEvent events on the day button control on date selection/de-selection. We will attach automation event handlers to the parent Calendar control for these events with scope as TreeScope.Children (i.e. listen to the specified events from Calendar and its children).

 

Pseudo code:

 

 

AddEventHandler(IUITechnologyElement element,

                UITestEventType eventType, IUITestEventNotify eventSink)

 

       // SourceElement is set to the element for which AddEventHandler was invoked.

AutomationElement sourceElement = element;

AutomationElement targetElement;

 

if (element.ControlType == ControlType.Button &&

    HasParent(element) && element.Parent.ControlType == ControlType.Calendar)

{

    // ValueChange notification will be done on the Calendar control; hence set it as the target element.

targetElement = element.Parent;

 

// Attach Automation handlers to the Calendar control.

    AutomationEventHandler calendarEventHandler = new AutomationEventHandler(OnSelected);

 

    Automation.AddAutomationEventHandler(SelectionItemPattern.ElementAddedToSelectionEvent,

        calendarAutomationPeer, TreeScope.Children, calendarEventHandler);

    Automation.AddAutomationEventHandler(SelectionItemPattern. ElementRemovedFromSelectionEvent,

        calendarAutomationPeer, TreeScope.Children, calendarEventHandler);

    Automation.AddAutomationEventHandler(SelectionItemPattern. ElementSelectedEvent,

        calendarAutomationPeer, TreeScope.Children, calendarEventHandler);

}

 

 

// The event handler

OnSelected(object sender, AutomationEventArgs e)

 

  if (sourceElement not defined)

  sourceElement = CreateUiaTechnologyElement(sender as AutomationElement);

 

 

// Get the selected items from the calendar control using SelectionPattern.

List<string> selectedItems = GetSelectedItemsUsingSelectionPattern(targetElement);

String selectedDates = BuildCommaSeparatedDateString(selectionItems);

 

eventSink.Notify(sourceElement, targetElement, UITestEventType.ValueChanged, selectedDates);

 

 

 

 

As an example, when user clicks on “16-Dec 2009” button, the notification would look something like eventSink.Notify(16dec2009ButtonElement, calendarElement, ValueChanged, “16-Dec-2009”)

On receiving the ValueChanged notification, the recorder will push “SetValueAction(selectedDates) on CalendarControl” into the action stack. This will trigger a new loop of aggregation.

 

 

III.                Aggregation

 

In this particular scenario, we do not intend to record any raw user actions. Hence, the MouseButtonClick on the day button needs to be aggregated out by the subsequent SetValue action. To accomplish this, we will write a custom aggregator, say WpfCalendarSetValueAggregator class extending from UITestActionFilter class and implement the ProcessRule() method.

 

[Note: UITest has some of the common aggregation logic in-built, so it may not be always required to write a custom aggregator. This example is just for sake of clarity].

 

Pseudo Code:

 

 

ProcessRule(IUITestActionStack actionStack)

 

        SetValueAction valueAction = actions.Peek() as SetValueAction;

        ReturnIfActionOrItsUIElementIsNull(valueAction);

        DoAggregation = false;

 

        // Iterate till aggregation succeeds i.e. attempt to aggregate out multiple user actions preceding the

        // associated SetValue action. For example, if user navigates across months by clicking on the navigation

        // button and then selects a date, all click actions needs to be aggregated out.

 

        do

        {

            MouseAction mouseAction = actions.Peek(1) as MouseAction;

            ReturnIfActionOrItsUIElementIsNull(mouseAction);

 

            if ( valueAction.UIElement.ControlType == ControlType.Calendar )

                if ( IfDefined(valueAction.SourceElement) && inputAction.UIElement == valueAction.SourceElement )

    DoAggregation = true;

                else if ( IsDescendanttOf(valueAction.UIElement, inputAction.UIElement) )

    DoAggregation = true;

 

                if (DoAggregation)

    // Remove the mouse action preceding the SetValue action.

    actions.Pop(1);

    // Set aggregation status, else the action will be removed by an inbuilt aggregated assuming it is a

    // bogus action (i.e. a SetValue action not preceded by any associated user action)

    valueAction.AdditionalInfo = "Aggregated";

 

          } while (DoAggregation);

 

 

 

 

IV.               Query ID generation

 

The Query ID or the search condition needs to be defined such that the playback search is fast as well as resilient. Resilience implies that with some minimal changes in the application under test, the pre-recorded script should still playback. The final goal however is to reach a balance between performance and resilience since they both typically contradict each other.

 

The Query ID that we will generate in this scenario will have the hierarchy as

[TopLevelWindow à ParentOfCalendar à WpfCalendar] i.e. 3 levels of hierarchy including the target element.

 

This is the common Query ID specified for technology elements. However, different controls across different technologies will have their own requirement and hence the search condition needs to be customized accordingly. For example, a Tree Item needs to generate all its ancestor Tree items till the Tree control (and add SearchConfiguration.ExpandWhileSearching to the queryId.SearchConfigurations[] search configuration of all expandable ancestors).  

 

The Query ID generation should be ideally cached on first use in the UITechnologyElement.QueryId implementation.

 

The decision behind arriving at a 3-level Query ID hierarchy (in normal scenarios), as mentioned above, is to find a balance between performance and resilience. Including the entire ancestor hierarchy in the Query ID will make the Query ID less resilient to change since any modification to an intermediate control will render the Query ID invalid.

 

Similarly, our playback follows a Breadth First Search (BFS) algorithm to search for controls starting from the Top Level Window. [The Top Level Window is searched using a technology agnostic EnumChildWindows search on the desktop]. Having just the target control and the Top level window will limit the search refining at each level of BFS and hence affect performance.

 

Generating an “Instance” search condition is an effective to disambiguate a control within its parent when the control does not have any unique search property.

 

Finally, following are the search configurations that could be associated with a control’s search condition:

 

1.       ExpandWhileSearching –

Add SearchConfiguration.ExpandWhileSearching to the queryId.SearchConfigurations[] list for any intermediate element in queryID that is expandable. For example menu items and tree items are expandable. So if you have a queryId generated as A.B.C.D.E , where A is top level element, B is a treeview, and C,D,E are tree items, then C and D need to have  SearchConfiguration.ExpandWhileSearching set in their QueryId. SearchConfigurations. This will ensure that during playback search, after search for C is done, C is expanded prior to proceeding with search for D. Same for D, before searching for E.

 

2.       DisambiguateChild –

If both parent and child have same search conditions, set SearchConfiguration.DisambiguateChild to the queryId.SearchConfigurations[] for the child element.

For example, if have a tree item hierarchy as A à B à B, make sure the child B has SearchConfiguration.DisambiguateChild set.

 

3.       NextSibling –

This is a bit tricky and will be required if a control does not have a good enough distinct property for its search condition.

Consider a control A having children B, C, D in the same sequence. If D does not have a good property, but C has a good property; you can generate C as a ancestor for D and append SearchConfiguration.NextSibling to D’s search configuration. C is called the NextSibling ancestor of D. [Of course one option is to generate Instance=3 for D’s search condition using its Control Type. However, NextSibling is typically faster perf-wise for large number of children.]

There can be more complicated scenario for generating NextSibing. Say both B and C also do not have a good enough property. So check A’s previous siblings’s children for NextSibling ancestor.

 

4.       VisibleOnly –

Typically to speed up search. Do not include this in search configuration if it is required to search hidden elements.

 

 

 

PLAYBACK

 

Following sections explains the key points that need to be addressed while inserting a new control support in the technology’s property provider. (UITestPropertyProvider)

 

I.                    Specifying the control support level

 

Typically, the plugin writer would provide a ControlSupport.NativeSupport for all controls in the technology. There is however an option to provide a higher control level support for the control. For example, for specifying control level support for the WpfCalendar, we would implement the following in GetControlSupportLevel():

 

 

            if (thisProvider.technologyName == uiControl.TechnologyName)

                if (uiControl.TechnologyElement.ControlType == ControlType.Calendar)

                    supportLevel = ControlSupport.ControlSpecificSupport;

                else

                    supportLevel = ControlSupport.NativeSupport;

 

 

 

II.                  Defining a new control class in PropertyProvider

 

Each UITestControl supports some properties common to all controls, such as BoundingRectangle, ControlType, Name, etc (defined in UITestControl.PropertyNames). In addition to this, a control can define its own control specific properties.

 

For our scenario, we will define a new class WpfCalendar that extends UITestControl. [As a good design, a plugin writer would first define a technology specific class WpfControl that extends UITestControl. This would define the properties common to all controls for the technology. The control specific property implementation can then be done by extending from WpfControl. However, for brevity I will skip the WpfControl hierarchy here]

 

 

We define the class as

 

namespace Microsoft.VisualStudio.TestTools.UITesting.WpfControls

{

public class WpfCalendar : UITestControl

{

        public WpfCalendar() :

            this(null)

        {

        }

 

        public WpfCalendar(UITestControl parent) :

            base(parent)

        {

            SearchProperties.Add("ControlType", "Calendar");

        }

}

 

// Additional property implementation here …

 

}

 

 

 

The namespace and className strings will be evident when we describe the code generation in the last section.

 

Let us assume we want to support two control specific properties – WpfCalendar.PropertyNames.SelectedDates

WpfCalendar.PropertyNames.MultiSelectable.

 

The nested class WfpCalendar.PropertyNames would inherit from UITestControl.PropertyNames.

 

 

 

WpfCalendar.PropertyNames

 

        new public abstract class PropertyNames : UITestControl.PropertyNames

        {

            /// <summary>

            /// Gets or sets the list of dates to be selected on a calendar control.

            /// Data-Type: System.String

            /// Format: {"dd-MMM-YYYY","dd-MMM-YYYY",...}

            /// </summary>

            public static readonly string SelectedDates = "SelectedDates"; //Writable

 

            /// <summary>

            /// Gets the multi-selectable state of calendar control.

            /// Data-Type: System.Boolean

            /// </summary>

            public static readonly string MultiSelectable = "MultiSelectable";

        }

 

 

These properties will be exposed from the UICL, unless they have UITestPropertyAttributes.NonAssertable attribute set explicitly in its UITestPropertyDescriptor.

 

We will also define the Get/Set properties within the WpfCalendar class.

 

       

        /// <summary>

        /// Gets or sets the list of dates to be selected on a calendar control.

        /// Data-Type: System.String

        /// Format: {"dd-MMM-YYYY","dd-MMM-YYYY",...}

        /// </summary>

        public virtual string SelectedDates

        {

            get

            {

                return ((string)(GetPropertyInternal(WpfCalendar.PropertyNames.SelectedDates)));

            }

            set

            {

                SetPropertyInternal(WpfCalendar.PropertyNames.SelectedDates, value);

            }

        }

 

        /// <summary>

        /// Gets the multi-selectable state of calendar control.

        /// </summary>

        public virtual bool MultiSelectable

        {

            get

            {

                return ((bool)(GetPropertyInternal(WpfCalendar.PropertyNames.MultiSelectable)));

            }

        }

 

 

 

III.                Define the control mappings

 

The following two mappings are required to be maintained internally –

 

a.       {ControlType à PropertyNames}, to respond to UITestPropertyProvider.GetPropertyNames(UITestControl uiTestControl) invocation.

b.       {PropertyName à PropertyDescriptor], to respond to UITestPropertyProvider.GetPropertyDescriptor(UITestControl uiTestControl, string propertyName) invocation.

 

For the WpfCalendar, we will add the following mapping at the property provider initialization –

 

 

Dictionary<string, UITestPropertyDescriptor> controlSpecificProperties =

    new Dictionary<string, UITestPropertyDescriptor>();

 

                controlSpecificProperties.Add(WpfCalendar.PropertyNames.SelectedDates,

                    new UITestPropertyDescriptor(typeof(string),

                                 UITestPropertyAttributes.Readable | UITestPropertyAttributes.Writable));

                controlSpecificProperties.Add(WpfCalendar.PropertyNames.MultiSelectable,

                    new UITestPropertyDescriptor(typeof(bool),

                                 UITestPropertyAttributes.Readable));

 

Dictionary<ControlType, Dictionary<string, UITestPropertyDescriptor>> propertyMap =

    new Dictionary<ControlType,  Dictionary<string, UITestPropertyDescriptor>>();

 

propertiesMap.Add(ControlType.Calendar, controlSpecificProperties);

 

 

 

IV.                Implementing the property getter and setter

 

The implementation will be specific to the technology and control. In this case, we will rely on the Pattern support provided by the UI Automation to query for the native Automation element properties or perform any actions.

 

 

GetPropertyValue(UITestControl uiTestControl, string propertyName)

 

            if (uiControl.ControlType == ControlType.Calendar)

            {

                AutomationElement calendar =

                    this.UIControl.TechnologyElement.NativeElement as AutomationElement;

               

                // Selection pattern provides information on the current selection in the calendar.

                SelectionPattern selPattern = GetSelectionPattern(calendar);

 

               // Here, both Value and SelectedDates properties equate to get selection.

                if (UITestControl.PropertyNames.Value == propertyName ||

                    WpfCalendar.PropertyNames.SelectedDates == propertyName)

                     List<string> selections = GetSelectedItemsUsingPattern (selPattern);

                     return ConvertToCommaSeparatedDates(selections);

               

                else if (WpfCalendar.PropertyNames.MultiSelectable == propertyName)

                    return selPattern.Current.CanSelectMultiple;

            }

 

 

 

 

SetPropertyValue(UITestControl uiTestControl, string propertyName, object value)

 

            AutomationElement calendar = this.UIControl.TechnologyElement.NativeElement as AutomationElement;

 

            if (UITestControl.PropertyNames.Value == propertyName ||

                WpfCalendar.PropertyNames.SelectedDates == propertyName)

            {

                List<string> dates = ParseToCommaSeparatedDateStrings(propertyValue);

                int selectionCount = 0;

                foreach (string dateValue in dates)

                {

                    // Use UI Automation patterns to search and navigate directly to the month

                    // containing the ‘dateValue’ date.

                    NavigateToMonthView(dateValue);

 

                    // Create a UITestControl  and do a search to populate its inner reference to the Screen element.

                    UITestControl dateButton = new UITestControl(this.UIControl);

                    dateButton.TechnologyName = technologyName;

                    dateButton.SearchProperties.Add(UITestControl.PropertyNames.Name,  dateValue);

                    dateButton.Find();

 

                    // Set the modifier keys appropriately based on single or multiple selection.

                    modifierKeys = (selectionCount++ > 0) ? ModifierKeys.None : ModifierKeys.Control;

 

                    // Do click for selection.

                    dateButton.Click(MouseButtons.Left, modifierKeys, new Point(-1, -1));

                }

            }

 

 

 

 

 

CODE GENERATION

 

 

For the CodeGenerator to generate specialized class objects in the UIMap, it invokes UITestPropertyProvider:GetSpecializedClass(UITestControl uiControl) to determine the type of specialized class. This method needs to be implemented appropriately in its property provider.

 

[Refer Playback section II for the namespace and class name of the new WPF Calendar class]

 

To return this specialized class type, following is the code snippet:

 

GetSpecializedClass(UITestControl uiControl)

 

ControlType controlType = uiControl.ControlType;

           

Type specializedType =

    Type.GetType(string.Format(CultureInfo.InvariantCulture, "{0}.{1}",

       Microsoft.VisualStudio.TestTools.UITesting.WpfControls, “Wpf” +

       controlType.Name));

 

return specializedType;

 

 

For the WPF Calendar, it will return Type of Microsoft.VisualStudio.TestTools.UITesting.WpfControls.WpfCalendar.

 

Secondly, the UITestPropertyProvider method

public abstract string[] GetPredefinedSearchProperties(Type specializedClass);

can be overridden to define the default search properties in the specialized class.

 

For the Calendar, we will set the UITestControl.PropertyNames.ControlType as the default search property i.e.

 

 

return new string[] { UITestControl.PropertyNames.ControlType };

 

 

Following is a sample code generated from a recorded action of click on a date button in WPF Calendar control:

 

    using System;

    using System.CodeDom.Compiler;

    using System.Collections.Generic;

    using System.Drawing;

    using System.Text.RegularExpressions;

    using System.Windows.Input;

    using Microsoft.VisualStudio.TestTools.UITest.Extension;

    using Microsoft.VisualStudio.TestTools.UITesting;

    using Microsoft.VisualStudio.TestTools.UITesting.WpfControls;

    using Microsoft.VisualStudio.TestTools.UnitTesting;

    using Keyboard = Microsoft.VisualStudio.TestTools.UITesting.Keyboard;

    using Mouse = Microsoft.VisualStudio.TestTools.UITesting.Mouse;

    using MouseButtons = System.Windows.Forms.MouseButtons;

   

   

    [GeneratedCode("Coded UITest Builder", "10.0.21208.0")]

    public partial class UIMap

    {

       

        #region Fields

        private RecordedMethodParams mRecordedMethodParams;

       

        private UIDatePickerCalendarWindow mUIDatePickerCalendarWindow;

        #endregion

       

        #region Properties

        public virtual RecordedMethodParams RecordedMethodParams

        {

            get

            {

                if ((this.mRecordedMethodParams == null))

                {

                    this.mRecordedMethodParams = new RecordedMethodParams();

                }

                return this.mRecordedMethodParams;

            }

        }

       

        public UIDatePickerCalendarWindow UIDatePickerCalendarWindow

        {

            get

            {

                if ((this.mUIDatePickerCalendarWindow == null))

                {

                    this.mUIDatePickerCalendarWindow = new UIDatePickerCalendarWindow();

                }

                return this.mUIDatePickerCalendarWindow;

            }

        }

        #endregion

       

        /// <summary>

        /// RecordedMethod - Use 'RecordedMethodParams' to pass parameters into this method.

        /// </summary>

        public void RecordedMethod()

        {

            #region Variable Declarations

            WpfCalendar uICalendar1Calendar = this.UIDatePickerCalendarWindow.UICalendar1Calendar;

            #endregion

 

            // Select '17-Dec-2009' in 'Calendar1' calendar

            uICalendar1Calendar.SelectedDates = this.RecordedMethodParams.UICalendar1CalendarSelectedDates;

        }

    }

   

    /// <summary>

    /// Parameters to be passed into 'RecordedMethod'

    /// </summary>

    [GeneratedCode("Coded UITest Builder", "10.0.21208.0")]

    public class RecordedMethodParams

    {

       

        #region Fields

        /// <summary>

        /// Select '17-Dec-2009' in 'Calendar1' calendar

        /// </summary>

        public string UICalendar1CalendarSelectedDates = "17-Dec-2009";

        #endregion

    }

   

    [GeneratedCode("Coded UITest Builder", "10.0.21208.0")]

    public class UIDatePickerCalendarWindow : WpfWindow

    {

       

        #region Fields

        private WpfCalendar mUICalendar1Calendar;

        #endregion

       

        public UIDatePickerCalendarWindow()

        {

            #region Search Criteria

            this.SearchProperties[WpfWindow.PropertyNames.Name] = "DatePickerCalendar";

            this.SearchProperties.Add(new PropertyExpression(WpfWindow.PropertyNames.ClassName, "HwndWrapper", PropertyExpressionOperator.Contains));

            this.WindowTitles.Add("DatePickerCalendar");

            #endregion

        }

       

        #region Properties

        public WpfCalendar UICalendar1Calendar

        {

            get

            {

                if ((this.mUICalendar1Calendar == null))

                {

                    this.mUICalendar1Calendar = new WpfCalendar(this);

                    #region Search Criteria

                    this.mUICalendar1Calendar.SearchProperties[WpfCalendar.PropertyNames.AutomationId] = "Calendar1";

                    this.mUICalendar1Calendar.WindowTitles.Add("DatePickerCalendar");

                    #endregion

                }

                return this.mUICalendar1Calendar;

            }

        }

        #endregion

    }