Browse Source

Added access/accelerator key support.

And start adding integration tests for menu items.
ui-automation-test
Steven Kirk 5 years ago
parent
commit
06ed896317
  1. 108
      samples/IntegrationTestApp/MainWindow.axaml
  2. 16
      src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
  3. 16
      src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs
  4. 2
      src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs
  5. 2
      src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
  6. 37
      src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs
  7. 38
      src/Avalonia.Controls/Primitives/AccessText.cs
  8. 2
      src/Windows/Avalonia.Win32/Automation/AutomationNode.cs
  9. 8
      tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs
  10. 31
      tests/Avalonia.IntegrationTests.Win32/MenuTests.cs

108
samples/IntegrationTestApp/MainWindow.axaml

@ -5,56 +5,64 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="IntegrationTestApp.MainWindow"
Title="IntegrationTestApp">
<TabControl TabStripPlacement="Left" Name="MainTabs">
<TabItem Header="Automation">
<StackPanel>
<TextBlock Name="TextBlockWithName">TextBlockWithName</TextBlock>
<TextBlock Name="NotTheAutomationId" AutomationProperties.AutomationId="TextBlockWithNameAndAutomationId">
TextBlockWithNameAndAutomationId
</TextBlock>
<TextBlock Name="TextBlockAsLabel">Label for TextBox</TextBlock>
<TextBox Name="LabeledByTextBox" AutomationProperties.LabeledBy="{Binding #TextBlockAsLabel}">
Foo
</TextBox>
</StackPanel>
</TabItem>
<TabItem Header="Button">
<StackPanel>
<Button Name="DisabledButton" IsEnabled="False">
Disabled Button
</Button>
<Button Name="BasicButton">
Basic Button
</Button>
<Button Name="ButtonWithTextBlock">
<TextBlock>Button with TextBlock</TextBlock>
</Button>
</StackPanel>
</TabItem>
<DockPanel>
<Menu DockPanel.Dock="Top">
<MenuItem Name="FileMenu" Header="_File">
<MenuItem Name="OpenMenu" Header="_Open..." InputGesture="Ctrl+O"/>
</MenuItem>
</Menu>
<TabControl TabStripPlacement="Left" Name="MainTabs">
<TabItem Header="Automation">
<StackPanel>
<TextBlock Name="TextBlockWithName">TextBlockWithName</TextBlock>
<TextBlock Name="NotTheAutomationId" AutomationProperties.AutomationId="TextBlockWithNameAndAutomationId">
TextBlockWithNameAndAutomationId
</TextBlock>
<TextBlock Name="TextBlockAsLabel">Label for TextBox</TextBlock>
<TextBox Name="LabeledByTextBox" AutomationProperties.LabeledBy="{Binding #TextBlockAsLabel}">
Foo
</TextBox>
</StackPanel>
</TabItem>
<TabItem Header="Button">
<StackPanel>
<Button Name="DisabledButton" IsEnabled="False">
Disabled Button
</Button>
<Button Name="BasicButton">
Basic Button
</Button>
<Button Name="ButtonWithTextBlock">
<TextBlock>Button with TextBlock</TextBlock>
</Button>
<Button Name="ButtonWithAcceleratorKey" HotKey="Ctrl+B">Button with Accelerator Key</Button>
</StackPanel>
</TabItem>
<TabItem Header="CheckBox">
<StackPanel>
<CheckBox Name="UncheckedCheckBox">Unchecked</CheckBox>
<CheckBox Name="CheckedCheckBox" IsChecked="True">Checked</CheckBox>
<CheckBox Name="ThreeStateCheckBox" IsThreeState="True" IsChecked="{x:Null}">ThreeState</CheckBox>
</StackPanel>
</TabItem>
<TabItem Header="CheckBox">
<StackPanel>
<CheckBox Name="UncheckedCheckBox">Unchecked</CheckBox>
<CheckBox Name="CheckedCheckBox" IsChecked="True">Checked</CheckBox>
<CheckBox Name="ThreeStateCheckBox" IsThreeState="True" IsChecked="{x:Null}">ThreeState</CheckBox>
</StackPanel>
</TabItem>
<TabItem Header="ComboBox">
<StackPanel>
<ComboBox Name="UnselectedComboBox">
<ComboBoxItem>Foo</ComboBoxItem>
<ComboBoxItem>Bar</ComboBoxItem>
</ComboBox>
<ComboBox Name="SelectedIndex0ComboBox" SelectedIndex="0">
<ComboBoxItem>Foo</ComboBoxItem>
<ComboBoxItem>Bar</ComboBoxItem>
</ComboBox>
<ComboBox Name="SelectedIndex1ComboBox" SelectedIndex="1">
<ComboBoxItem>Foo</ComboBoxItem>
<ComboBoxItem>Bar</ComboBoxItem>
</ComboBox>
</StackPanel>
</TabItem>
</TabControl>
<TabItem Header="ComboBox">
<StackPanel>
<ComboBox Name="UnselectedComboBox">
<ComboBoxItem>Foo</ComboBoxItem>
<ComboBoxItem>Bar</ComboBoxItem>
</ComboBox>
<ComboBox Name="SelectedIndex0ComboBox" SelectedIndex="0">
<ComboBoxItem>Foo</ComboBoxItem>
<ComboBoxItem>Bar</ComboBoxItem>
</ComboBox>
<ComboBox Name="SelectedIndex1ComboBox" SelectedIndex="1">
<ComboBoxItem>Foo</ComboBoxItem>
<ComboBoxItem>Bar</ComboBoxItem>
</ComboBox>
</StackPanel>
</TabItem>
</TabControl>
</DockPanel>
</Window>

16
src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Automation.Platform;
@ -75,6 +76,17 @@ namespace Avalonia.Automation.Peers
/// </summary>
public void BringIntoView() => BringIntoViewCore();
/// <summary>
/// Gets the accelerator key combinations for the element that is associated with the UI
/// Automation peer.
/// </summary>
public string? GetAcceleratorKey() => GetAcceleratorKeyCore();
/// <summary>
/// Gets the access key for the element that is associated with the automation peer.
/// </summary>
public string? GetAccessKey() => GetAccessKeyCore();
/// <summary>
/// Gets the control type for the element that is associated with the UI Automation peer.
/// </summary>
@ -208,6 +220,8 @@ namespace Avalonia.Automation.Peers
}
protected abstract void BringIntoViewCore();
protected abstract string? GetAcceleratorKeyCore();
protected abstract string? GetAccessKeyCore();
protected abstract AutomationControlType GetAutomationControlTypeCore();
protected abstract string? GetAutomationIdCore();
protected abstract Rect GetBoundingRectangleCore();

16
src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs

@ -13,13 +13,27 @@ namespace Avalonia.Automation.Peers
: base(factory, owner)
{
}
public new Button Owner => (Button)base.Owner;
public void Invoke()
{
EnsureEnabled();
(Owner as Button)?.PerformClick();
}
protected override string? GetAcceleratorKeyCore()
{
var result = base.GetAcceleratorKeyCore();
if (string.IsNullOrWhiteSpace(result))
{
result = Owner.HotKey?.ToString();
}
return result;
}
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.Button;

2
src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs

@ -92,6 +92,8 @@ namespace Avalonia.Automation.Peers
}
}
protected override string? GetAcceleratorKeyCore() => null;
protected override string? GetAccessKeyCore() => null;
protected override string? GetAutomationIdCore() => null;
protected override string GetClassNameCore() => typeof(ComboBoxItem).Name;
protected override AutomationPeer? GetLabeledByCore() => null;

2
src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs

@ -148,6 +148,8 @@ namespace Avalonia.Automation.Peers
return true;
}
protected override string? GetAcceleratorKeyCore() => AutomationProperties.GetAcceleratorKey(Owner);
protected override string? GetAccessKeyCore() => AutomationProperties.GetAccessKey(Owner);
protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner) ?? Owner.Name;
protected override Rect GetBoundingRectangleCore() => GetBounds(Owner.TransformedBounds);
protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom;

37
src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs

@ -1,5 +1,6 @@
using Avalonia.Automation.Platform;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
#nullable enable
@ -14,6 +15,33 @@ namespace Avalonia.Automation.Peers
public new MenuItem Owner => (MenuItem)base.Owner;
protected override string? GetAccessKeyCore()
{
var result = base.GetAccessKeyCore();
if (string.IsNullOrWhiteSpace(result))
{
if (Owner.HeaderPresenter.Child is AccessText accessText)
{
result = accessText.AccessKey.ToString();
}
}
return result;
}
protected override string? GetAcceleratorKeyCore()
{
var result = base.GetAcceleratorKeyCore();
if (string.IsNullOrWhiteSpace(result))
{
result = Owner.InputGesture?.ToString();
}
return result;
}
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.MenuItem;
@ -23,14 +51,9 @@ namespace Avalonia.Automation.Peers
{
var result = base.GetNameCore();
if (result is null && Owner.HeaderPresenter.Child is TextBlock text)
{
result = text.Text;
}
if (result is null)
if (result is null && Owner.Header is string header)
{
result = Owner.Header?.ToString();
result = AccessText.RemoveAccessKeyMarker(header);
}
return result;

38
src/Avalonia.Controls/Primitives/AccessText.cs

@ -1,4 +1,6 @@
using System;
using Avalonia.Automation.Peers;
using Avalonia.Automation.Platform;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
@ -76,6 +78,37 @@ namespace Avalonia.Controls.Primitives
}
}
internal static string RemoveAccessKeyMarker(string text)
{
if (!string.IsNullOrEmpty(text))
{
var accessKeyMarker = "_";
var doubleAccessKeyMarker = accessKeyMarker + accessKeyMarker;
int index = FindAccessKeyMarker(text);
if (index >= 0 && index < text.Length - 1)
text = text.Remove(index, 1);
text = text.Replace(doubleAccessKeyMarker, accessKeyMarker);
}
return text;
}
private static int FindAccessKeyMarker(string text)
{
var length = text.Length;
var startIndex = 0;
while (startIndex < length)
{
int index = text.IndexOf('_', startIndex);
if (index == -1)
return -1;
if (index + 1 < length && text[index + 1] != '_')
return index;
startIndex = index + 2;
}
return -1;
}
/// <summary>
/// Get the pixel location relative to the top-left of the layout box given the text position.
/// </summary>
@ -180,6 +213,11 @@ namespace Avalonia.Controls.Primitives
}
}
protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
{
return new NoneAutomationPeer(factory, this);
}
/// <summary>
/// Returns a string with the first underscore stripped.
/// </summary>

2
src/Windows/Avalonia.Win32/Automation/AutomationNode.cs

@ -116,6 +116,8 @@ namespace Avalonia.Win32.Automation
{
return (UiaPropertyId)propertyId switch
{
UiaPropertyId.AcceleratorKey => InvokeSync(() => Peer.GetAcceleratorKey()),
UiaPropertyId.AccessKey => InvokeSync(() => Peer.GetAccessKey()),
UiaPropertyId.AutomationId => InvokeSync(() => Peer.GetAutomationId()),
UiaPropertyId.ClassName => InvokeSync(() => Peer.GetClassName()),
UiaPropertyId.ClickablePoint => new[] { BoundingRectangle.Center.X, BoundingRectangle.Center.Y },

8
tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs

@ -42,5 +42,13 @@ namespace Avalonia.IntegrationTests.Win32
Assert.Equal("Button with TextBlock", button.Text);
}
[Fact]
public void ButtonWithAcceleratorKey()
{
var button = _session.FindElementByAccessibilityId("ButtonWithAcceleratorKey");
Assert.Equal("Ctrl+B", button.GetAttribute("AcceleratorKey"));
}
}
}

31
tests/Avalonia.IntegrationTests.Win32/MenuTests.cs

@ -0,0 +1,31 @@
using OpenQA.Selenium.Appium.Windows;
using Xunit;
namespace Avalonia.IntegrationTests.Win32
{
[Collection("Default")]
public class MenuTests
{
private WindowsDriver<WindowsElement> _session;
public MenuTests(TestAppFixture fixture) => _session = fixture.Session;
[Fact]
public void File()
{
var fileMenu = _session.FindElementByAccessibilityId("FileMenu");
Assert.Equal("File", fileMenu.Text);
}
[Fact]
public void Open()
{
var fileMenu = _session.FindElementByAccessibilityId("FileMenu");
fileMenu.Click();
var openMenu = fileMenu.FindElementByAccessibilityId("OpenMenu");
Assert.Equal("Ctrl+O", openMenu.GetAttribute("AcceleratorKey"));
}
}
}
Loading…
Cancel
Save