Coded UI Test Extension for 3rd party Windows Forms controls–How to?

Coded UI Test Extension for 3rd party Windows Forms controls–How to?

  • Comments 4

Here is how one can add FULL Coded UI Test support for a 3rd party control based on Windows Forms technology.

Implementation Method – Accessibility

Accessibility means having equal access to web-based information and services regardless of physical or developmental abilities or impairments. Once implemented, it helps developers make their programs more compatible with accessibility aids (help users with impairment to use programs more effectively).

Microsoft has developed two accessibility standards - Microsoft Active Accessibility (MSAA) and Microsoft UI Automation (UIA)

MSAA is designed to help Assistive Technology (AT) products interact with standard and custom user interface (UI) elements of an application (or the operating system), as well as to access, identify, and manipulate an application's UI elements including enabling automation testing.

UIA is a newer technology that provides a much richer object model than MSAA, and is compatible with both Win32 and the .NET Framework. It is designed so that it can be supported across platforms other than Microsoft Windows.

We recommend MSAA for Windows Forms because Windows Forms natively supports MSAA and not UIA. Although there is MSAA to UIA & vice-versa conversion available via built-in proxy\bridge but the results in an extra layer of dependency which could eat into performance, hence MSAA.

Why Accessibility for Coded UI Test?

1. Implementing accessibility is simpler because one needs to follow established guidelines that serve the purpose of developing testable as well as accessible controls in one go

2. There is not much need to write an entire plugin for controls as much of the work is already done. We have built plugins for Windows Forms and Windows Presentation Foundation controls using accessibility technologies and have exposed appropriate extension points for easy hook-up

3. Vendors such as Telerik who develop 3rd party controls have already announced accessibility implementation

 

Detailed steps with reference to Coded UI Test Extension for 3rd party controls - the basics explained

Level 1 – Basic Record and Playback

How can I make a third party GridView with rows and cells to work with basic recording and playback of Coded UI Test?

1. Implement MSAA on existing control

image

2. Override Accessibility methods while you refer to http://msdn.microsoft.com/en-us/library/system.windows.forms.accessibleobject_methods.aspx

image

3. Example GetChild implementation

public override AccessibleObject GetChild(int index)
{
    if (index >= 0 && index < this.GetChildCount())
    {
        GridViewCellInfo cellInFocus = row.Cells[index];
        RectangleF rectangle = GetCellElement(row, owner.Columns[index]);
        return new CellAccessibleObject(owner, cellInFocus,
            owner.TableElement.ViewElement.RowLayout.RenderColumns[index+1],
            this, new Point((int)rectangle.X, (int)rectangle.Y),
            new Size((int)rectangle.Width, (int)rectangle.Height),
            row.Cells[index].Value);
    }
    return null;
}

Level 2 – Rich Property Validation

How to add BackColor / ForeColor / BorderColor properties of the Cell so I can assert on the values using the Coded UI Test Builder?

1. Override Description method of AccessibleObject and pass rich property values as semi-colon separated string. This modification needs to be added in the custom control code.

public override string Description
{
    get
    {
        return "GridViewCellInfo ;" + cell.Value+";"+this.cell.Style.BackColor.A + ", " +
            this.cell.Style.BackColor.B + ", " + cell.Style.BackColor.G + ";" + this.cell.Style.ForeColor.A + ", " +
            this.cell.Style.ForeColor.B + ", " + cell.Style.ForeColor.G + ";" + this.cell.Style.BorderColor.A + ", " +
            this.cell.Style.BorderColor.B + ", " + cell.Style.BorderColor.G+";"+cell.IsSelected;
    }
}

 

2. A plugin needs to be written on top of PropertyProvider. Write custom PropertyProvider and implement GetPropertyValue method. Use this method to retrieve the semi-colon separated property values

public override object GetPropertyValue(UITestControl uiTestControl, string propertyName)
{
    if (CellPropertiesMap.ContainsKey(propertyName) &&
        (CellPropertiesMap[propertyName].Attributes &
            UITestPropertyAttributes.Readable) == UITestPropertyAttributes.Readable)
    {
        object[] native = uiTestControl.NativeElement as object[];
        IAccessible a = native[0] as IAccessible;

        if (string.Equals(propertyName, GridViewCellInfo.PropertyNames.BackColor, StringComparison.OrdinalIgnoreCase))
        {
            string[] descriptionTokens = a.accDescription.Split(new char[] { ';' }); ;
            return descriptionTokens[2];
        }
        if (string.Equals(propertyName, GridViewCellInfo.PropertyNames.ForeColor, StringComparison.OrdinalIgnoreCase))
        {
            string[] descriptionTokens = a.accDescription.Split(new char[] { ';' }); ;
            return descriptionTokens[3];
        }
        if (string.Equals(propertyName, GridViewCellInfo.PropertyNames.BorderColor, StringComparison.OrdinalIgnoreCase))
        {
            string[] descriptionTokens = a.accDescription.Split(new char[] { ';' }); ;
            return descriptionTokens[4];
        }
        throw new NotSupportedException();
    }
    isInThisProvider = true;
    try
    {
        UITestControl winControl = new UITestControl();
        winControl.CopyFrom(uiTestControl);
        return winControl;
    }
    finally
    {
        isInThisProvider = false;
    }
}

3. Define a Dictionary of custom property names and UITestPropertyDescriptor property types

private static Dictionary<string, UITestPropertyDescriptor> CellPropertiesMap
{
    get
    {
        if (cellPropertiesMap == null)
        {
            UITestPropertyAttributes read = UITestPropertyAttributes.Readable | UITestPropertyAttributes.DoNotGenerateProperties;
            cellPropertiesMap = new Dictionary<string, UITestPropertyDescriptor>(StringComparer.OrdinalIgnoreCase);
            cellPropertiesMap.Add("BackColor", new UITestPropertyDescriptor(typeof(string), read));
            cellPropertiesMap.Add("ForeColor", new UITestPropertyDescriptor(typeof(string), read));
            cellPropertiesMap.Add("BorderColor", new UITestPropertyDescriptor(typeof(string), read));
        }

        return cellPropertiesMap;
    }
}

4. Override GetPropertyDescriptor and return the property types set in step 3

public override UITestPropertyDescriptor GetPropertyDescriptor(UITestControl uiTestControl, string propertyName)
{
    var map = GetAllPropertiesMap(uiTestControl);
    var map2 = GetCellPropertiesMap(uiTestControl);
    if (map2.ContainsKey(propertyName))
    {
        return map2[propertyName];
    }
    return null;
}

5. Override GetpropertyNames of PropertyProvider

public override ICollection<string> GetPropertyNames(UITestControl uiTestControl)
{
    String controlType = uiTestControl.ControlType.Name;
    if (uiTestControl.ControlType.NameEquals("Cell"))
    {
        return GetCellPropertiesMap(uiTestControl).Keys;
    }
    return GetAllPropertiesMap(uiTestControl).Keys;
}

6. Override GetControlSupportLevel of PropertyProvider. The method should be set to return maximum Control Support for the custom control which in this case includes Table, Row and Cell

public override int GetControlSupportLevel(UITestControl uiTestControl)
{
    if (!isInThisProvider && IsSupported(uiTestControl))
    {
        return (int)ControlSupport.ControlSpecificSupport;
    }

    return (int)ControlSupport.NoSupport;
}
private static bool IsSupported(UITestControl uiTestControl)
{
    return (uiTestControl is RadGridView) || (uiTestControl is GridViewRowInfo) || (uiTestControl is GridViewCellInfo) || (uiTestControl is GridViewDataColumn) ||
    (string.Equals(uiTestControl.TechnologyName, "MSAA", StringComparison.OrdinalIgnoreCase) &&
    (uiTestControl.ControlType == ControlType.Table || uiTestControl.ControlType == "Row" || uiTestControl.ControlType == "Cell" ||
    uiTestControl.ControlType == "Edit"));

}

This completes the PropertyProvider implementation to add rich control validation. Now we need to add this service to the UITestExtension package.

7. Implement GetService of UITestExtensionPackage

internal class RadGridViewExtensionPackage : UITestExtensionPackage
{

    public override object GetService(Type serviceType)
    {
        if (serviceType == typeof(UITestPropertyProvider))
        {
            if (propertyProvider == null) { propertyProvider = new PropertyProvider(); }
           return propertyProvider;
        }
        return null;
    }

    public override void Dispose() {}
    public override string PackageDescription {get { return "RadGridView Extension"; }}
    public override string PackageName { get { return "RadGridView Extension"; } }
    public override string PackageVendor { get { return "Shubhra Maji"; }}
    public override Version PackageVersion { get { return new Version(1, 0); }  }
    public override Version VSVersion { get { return new Version(10, 0); } }

    private UITestPropertyProvider propertyProvider;
}

4. Build and deploy the binaries to "%CommonProgramFiles(x86)%\Microsoft Shared\VSTT\10.0\UITestExtensionPackages" directory

 

Level 3 – Code Generation

What do I need to do in order to see objects of custom classes in the auto-generated code after recording user actions?

1. Add specialized class for control extending from WinControl, in this example, Cell control (GridViewCellInfo)

2. Add Custom Property Names in the Specialized class if desired

public class GridViewCellInfo:WinControl
{

    public GridViewCellInfo(UITestControl c)
        : base(c)
    {
        SearchProperties.Add(UITestControl.PropertyNames.ControlType, "Cell");
    }

    public virtual string BackColor
    {
        get
        {
            return (string)this.GetProperty(PropertyNames.BackColor);
        }
    }

    new public abstract class PropertyNames : UITestControl.PropertyNames
    {
        public static readonly string BackColor = "BackColor";
    }
}

3. Override GetSpecializedClass method of PropertyProvider

public override Type GetSpecializedClass(UITestControl uiTestControl)
{
    if (uiTestControl.ControlType.NameEquals("Cell"))
    {
        return typeof(GridViewCellInfo);
    }
    return null;

}

4. Override GetPropertyNamesClassType method of PropertyProvider if you have defined custom properties for the control

public override Type GetPropertyNamesClassType(UITestControl uiTestControl)
{
    if (uiTestControl.ControlType.NameEquals("Cell"))
    {
        return typeof(GridViewCellInfo.PropertyNames);
    }
    return null;
}

This completes the PropertyProvider implementation to add custom code generation. Since we have already added the property provider service in Level 2, we need to re-deploy the binaries to "%CommonProgramFiles(x86)%\Microsoft Shared\VSTT\10.0\UITestExtensionPackages" directory

 

Level 4 – Intent Aware Actions

How can I aggregate a double click on a cell followed by edit text into one action - set value on cell?

Write aggregators for recording setValue and property provider for playing back the setValue.

1. Write custom ActionFilter and implement ProcessRule to filter actions

public override bool ProcessRule(IUITestActionStack actionStack)
{
    if (actionStack.Count < 2)
    {
        return false;
    }
    SetValueAction lastAction = actionStack.Peek() as SetValueAction;
    MouseAction mouseAction = actionStack.Peek(1) as MouseAction;
    if (lastAction != null && mouseAction != null)
    {
        if (lastAction.UIElement != null &&
            lastAction.UIElement.ControlTypeName.Equals(ControlType.Edit.ToString(), StringComparison.OrdinalIgnoreCase) &&
            mouseAction.UIElement != null &&
            mouseAction.UIElement.ControlTypeName.Equals(ControlType.Cell.ToString(), StringComparison.OrdinalIgnoreCase))
        {
            actionStack.Pop();
            UITestAction cellAction = actionStack.Pop();
            SetValueAction newCellAction = new SetValueAction(cellAction.UIElement, lastAction.Value);
            newCellAction.AdditionalInfo = "Aggregated";
            actionStack.Push(newCellAction);
        }
        return false;
    }

}

 

2. Override GetPropertyForAction  of PropertyProvider if you want to define custom action names

public override string GetPropertyForAction(UITestControl uiTestControl, UITestAction action)
{
    if (uiTestControl.ControlType.Equals(ControlType.Cell))
    {
        return GridViewCellInfo.PropertyNames.CellValue;
    }
    throw new NotSupportedException();
}

This completes the PropertyProvider implementation to add rich control validation. Now we need to add this service to the UITestExtension package.

3. Implement SetPropertyValue of PropertyProvider

public override void SetPropertyValue(UITestControl uiTestControl, string propertyName, object propertyValue)
{
    isInThisProvider = true;
    try
    {
        UITestControl copyControl = GetCopiedControl(uiTestControl);

        if (string.Equals(propertyName, GridEditTextBoxElement.PropertyNames.EditText, StringComparison.OrdinalIgnoreCase))
        {
            copyControl.SetProperty(WinEdit.PropertyNames.Text, propertyValue);
        }
        else if (string.Equals(propertyName, GridViewCellInfo.PropertyNames.CellValue, StringComparison.OrdinalIgnoreCase))
        {
            if (copyControl.ControlType.Equals(ControlType.Cell))
            {
                Mouse.Click(copyControl);
                try
                {
                    GridEditTextBoxElement editcontrol = new GridEditTextBoxElement(copyControl);
                    Mouse.Click(editcontrol);
                    Keyboard.SendKeys("{Home}");
                    Keyboard.SendKeys("{End}", System.Windows.Input.ModifierKeys.Shift);
                    Keyboard.SendKeys("{Delete}");
                    Keyboard.SendKeys(propertyValue.ToString());
                }
                catch (UITestControlNotFoundException)
                {
                    // retry after one more click
                    Mouse.Click(copyControl);

                    GridEditTextBoxElement editcontrol = new GridEditTextBoxElement(copyControl);
                    Mouse.Click(editcontrol);
                    Keyboard.SendKeys("{Home}");
                    Keyboard.SendKeys("{End}", System.Windows.Input.ModifierKeys.Shift);
                    Keyboard.SendKeys("{Delete}");
                    Keyboard.SendKeys(propertyValue.ToString());
                }
            }
        }
        else
        {
            GetCopiedControl(uiTestControl).SetProperty(propertyName, propertyValue);
        }
    }
    finally
    {
        isInThisProvider = false;
    }
}

4. Implement GetService of UITestExtensionPackage

internal class RadGridViewExtensionPackage : UITestExtensionPackage
{

    public override object GetService(Type serviceType)
    {
        if (serviceType == typeof(UITestPropertyProvider))
        {
            if (propertyProvider == null) { propertyProvider = new PropertyProvider(); }
           return propertyProvider;
        }
        else if (serviceType == typeof(UITestActionFilter))
        {
            if (actionFilter == null) { actionFilter = new RadGridViewActionFilter(); }
            return actionFilter;
        }
        return null;
    }

    public override void Dispose() {}
    private RadGridViewActionFilter actionFilter;
    public override string PackageDescription {get { return "RadGridView Extension"; }}
    public override string PackageName { get { return "RadGridView Extension"; } }
    public override string PackageVendor { get { return "Shubhra Maji"; }}
    public override Version PackageVersion { get { return new Version(1, 0); }  }
    public override Version VSVersion { get { return new Version(10, 0); } }

    private UITestPropertyProvider propertyProvider;
}

4. Build and deploy the binaries to "%CommonProgramFiles(x86)%\Microsoft Shared\VSTT\10.0\UITestExtensionPackages" directory

Leave a Comment
  • Please add 7 and 7 and type the answer here:
  • Post
  • error RG1000: Unknown build error, 'An item with the same key has already been added.'

    Please get visual studio team to print out the name duplicated resource in the *.sln file that cause this error.  This happens when merging C# project sln files in TFS.

  • Can you give this example in VB?

    Thanks, Steve

  • Thanks for the walk through but your examples are extremely "zoomed in" I don't understand the context of any of the code samples you provided. Do these go into the GUI you are trying to automate? Is that where the extension must happen? Is it possible to extend CUIT to an application if you do not have the source to it?

    Thanks for your help.

  • J,

    It is possible to extend CUIT for an application without having access to its source code. In my example, I have done the same. You can also refer to our recently released documentation on Enabling Coded UI for Custom Controls

    msdn.microsoft.com/.../hh552522.aspx

Page 1 of 1 (4 items)