Как определить настраиваемые элементы управления, чтобы включить автоматизацию пользовательского интерфейса и TestStack White?

Я начинаю использовать TestStack White (автоматизация пользовательского интерфейса) для автоматизации тестов в существующем приложении WPF. При использовании стандартных элементов управления все работает нормально. Однако я сталкиваюсь с проблемами при попытке взаимодействия с пользовательскими элементами управления.

Например, у меня есть LabeledComboBox, который на самом деле является TextBlock плюс ComboBox. Это определяется как класс, производный от Control плюс ControlTemplate в XAML:

public class LabeledComboBox : Control
{
    static LabeledComboBox()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(LabeledComboBox), new FrameworkPropertyMetadata(typeof(LabeledComboBox)));
    }
}

<local:LabeledComboBox>
    <local:LabeledComboBox.Template>
         <ControlTemplate TargetType="{x:Type local:LabeledComboBox}">
             <StackPanel>
                  <TextBlock Text="Text"/>
                  <ComboBox/>
             </StackPanel>
          </ControlTemplate>
    </local:LabeledComboBox.Template>
</local:LabeledComboBox>

Этот элемент управления работает, но если вы запустите автоматизацию пользовательского интерфейса, убедитесь, что единственная часть, видимая для автоматизации пользовательского интерфейса, — это ComboBox, а доступ к TextBlock недоступен.

Однако, если вы создаете это как UserControl с использованием XAML и кода программной части, TextBox и ComboBox должным образом видны для автоматизации пользовательского интерфейса.

Я попытался создать AutomationPeer (FrameworkElementAutomationPeer) для своего элемента управления, но пока не смог сделать TextBlock видимым для автоматизации пользовательского интерфейса. Одним из интересных результатов является то, что FrameworkElementAutomationPeer::GetChildrenCore() правильно возвращает список из двух одноранговых элементов автоматизации, один для TextBlock и один для ComboBox.

Как мне изменить свой пользовательский элемент управления, чтобы его можно было правильно протестировать с помощью UI Automation и White?


person Eduardo Hernández    schedule 06.11.2016    source источник


Ответы (1)


Одноранговый элемент автоматизации по умолчанию для TextBlock (TextBlockAutomationPeer) по какой-то причине удаляет соответствующего владельца из дерева пользовательского интерфейса, если он является частью ControlTemplate.

Вы можете найти соответствующий код здесь: https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Automation/Peers/TextBlockAutomationPeer.cs,16e7fab76ffcb40a

 override protected bool IsControlElementCore()
 {
    // Return true if TextBlock is not part of a ControlTemplate
    TextBlock tb = (TextBlock)Owner;
    DependencyObject templatedParent = tb.TemplatedParent;
    return templatedParent == null || templatedParent is ContentPresenter; // If the templatedParent is a ContentPresenter, this TextBlock is generated from a DataTemplate
 }

Итак, чтобы исправить это, вам придется объявить TextBlock не в ControlTemplate или обходным путем с помощью такого кода (трудно обобщить на все приложение...):

public class LabeledComboBox : Control
{
    static LabeledComboBox()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(LabeledComboBox), new FrameworkPropertyMetadata(typeof(LabeledComboBox)));
    }

    // define our own peer
    protected override AutomationPeer OnCreateAutomationPeer()
    {
        return new LabeledComboBoxAutomationPeer(this);
    }

    protected class LabeledComboBoxAutomationPeer : FrameworkElementAutomationPeer
    {
        public LabeledComboBoxAutomationPeer(LabeledComboBox owner) : base(owner)
        {
        }

        // replace all TextBlockAutomationPeer by our custom peer for TextBlock
        protected override List<AutomationPeer> GetChildrenCore()
        {
            var list = base.GetChildrenCore();
            for (int i = 0; i < list.Count; i++)
            {
                var tb = list[i] as TextBlockAutomationPeer;
                if (tb != null)
                {
                    list[i] = new InteractiveTextBlockAutomationPeer((TextBlock)tb.Owner);
                }
            }
            return list;
        }
    }

    // just do the default stuff, instead of the strange TextBlockAutomationPeer implementation
    protected class InteractiveTextBlockAutomationPeer : FrameworkElementAutomationPeer
    {
        public InteractiveTextBlockAutomationPeer(TextBlock owner) : base(owner)
        {
        }

        protected override AutomationControlType GetAutomationControlTypeCore()
        {
            return AutomationControlType.Text;
        }

        protected override string GetClassNameCore()
        {
            return "TextBlock";
        }
    }
}

Еще одно решение — создать свой собственный класс управления TextBlock (производный от TextBlock) и переопределить OnCreateAutomationPeer, чтобы вернуть пользовательский.

person Simon Mourier    schedule 07.11.2016
comment
Спасибо за объяснение, все решения работают правильно. Я нашел здесь дополнительную информацию: stackoverflow.com/a/2846419/5414769 - person Eduardo Hernández; 07.11.2016