Browse Source

Started adding ComboBox integration tests.

And fixed some issues with ComboBox/popups.
Steven Kirk 4 years ago
parent
commit
e8801c2aca

+ 3 - 0
samples/IntegrationTestApp/IntegrationTestApp.csproj

@@ -7,4 +7,7 @@
   <Import Project="..\..\build\BuildTargets.targets" />
   <Import Project="..\..\build\SampleApp.props" />
   <Import Project="..\..\build\ReferenceCoreLibraries.props" />
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
+  </ItemGroup>
 </Project>

+ 17 - 0
samples/IntegrationTestApp/MainWindow.axaml

@@ -8,6 +8,9 @@
   <TabControl TabStripPlacement="Left" AutomationProperties.AutomationId="MainTabs">
     <TabItem Header="Button">
       <StackPanel>
+        <Button AutomationProperties.AutomationId="DisabledButton" IsEnabled="False">
+            Disabled Button
+        </Button>
         <Button AutomationProperties.AutomationId="BasicButton">
             Basic Button
         </Button>
@@ -17,6 +20,20 @@
       </StackPanel>
     </TabItem>
     <TabItem Header="ComboBox">
+        <StackPanel>
+            <ComboBox AutomationProperties.AutomationId="UnselectedComboBox">
+                <ComboBoxItem>Foo</ComboBoxItem>
+                <ComboBoxItem>Bar</ComboBoxItem>
+            </ComboBox>
+            <ComboBox AutomationProperties.AutomationId="SelectedIndex0ComboBox" SelectedIndex="0">
+                <ComboBoxItem>Foo</ComboBoxItem>
+                <ComboBoxItem>Bar</ComboBoxItem>
+            </ComboBox>
+            <ComboBox AutomationProperties.AutomationId="SelectedIndex1ComboBox" SelectedIndex="1">
+                <ComboBoxItem>Foo</ComboBoxItem>
+                <ComboBoxItem>Bar</ComboBoxItem>
+            </ComboBox>
+        </StackPanel>
     </TabItem>
   </TabControl>
 </Window>

+ 1 - 0
samples/IntegrationTestApp/MainWindow.axaml.cs

@@ -9,6 +9,7 @@ namespace IntegrationTestApp
         public MainWindow()
         {
             InitializeComponent();
+            this.AttachDevTools();
         }
 
         private void InitializeComponent()

+ 3 - 0
src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs

@@ -78,5 +78,8 @@ namespace Avalonia.Automation.Peers
         {
             return AutomationControlType.ListItem;
         }
+
+        protected override bool IsContentElementCore() => true;
+        protected override bool IsControlElementCore() => true;
     }
 }

+ 52 - 0
src/Avalonia.Controls/Automation/Peers/PopupAutomationPeer.cs

@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Automation.Platform;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.VisualTree;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class PopupAutomationPeer : ControlAutomationPeer
+    {
+        public PopupAutomationPeer(IAutomationNodeFactory factory, Popup owner)
+            : base(factory, owner)
+        {
+            owner.Opened += PopupOpenedClosed;
+            owner.Closed += PopupOpenedClosed;
+        }
+
+        protected override IReadOnlyList<AutomationPeer>? GetChildrenCore()
+        {
+            var host = (IVisualTreeHost)Owner;
+            System.Diagnostics.Debug.WriteLine($"Popup children='{host}'");
+            return host.Root is Control c ? new[] { GetOrCreatePeer(c) } : null;
+        }
+
+        protected override bool IsContentElementCore() => false;
+        protected override bool IsControlElementCore() => false;
+
+        private void PopupOpenedClosed(object sender, EventArgs e)
+        {
+            // This is golden. We're following WPF's automation peer API here where the
+            // parent of a peer is set when another peer returns it as a child. We want to
+            // add the popup root as a child of the popup, so we need to return it as a
+            // child right? Yeah except invalidating children doesn't automatically cause
+            // UIA to re-read the children meaning that the parent doesn't get set. So the
+            // MAIN MECHANISM FOR PARENTING CONTROLS IS BROKEN WITH THE ONLY AUTOMATION API
+            // IT WAS WRITTEN FOR. Luckily WPF provides an escape-hatch by exposing the
+            // TrySetParent API internally to work around this. We're exposing it publicly
+            // to shame whoever came up with this abomination of an API.
+            GetPopupRoot()?.TrySetParent(this);
+            InvalidateChildren();
+        }
+
+        private AutomationPeer? GetPopupRoot()
+        {
+            var popupRoot = ((IVisualTreeHost)Owner).Root as Control;
+            return popupRoot is object ? GetOrCreatePeer(popupRoot) : null;
+        }
+    }
+}

+ 7 - 0
src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs

@@ -21,6 +21,13 @@ namespace Avalonia.Automation.Peers
         protected override bool IsContentElementCore() => false;
         protected override bool IsControlElementCore() => false;
 
+
+        protected override AutomationPeer? GetParentCore()
+        {
+            var parent = base.GetParentCore();
+            return parent;
+        }
+
         private void OnOpened(object sender, EventArgs e)
         {
             ((PopupRoot)Owner).Opened -= OnOpened;

+ 5 - 0
src/Avalonia.Controls/Primitives/Popup.cs

@@ -516,6 +516,11 @@ namespace Avalonia.Controls.Primitives
             Close();
         }
 
+        protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new PopupAutomationPeer(factory, this);
+        }
+
         private static IDisposable SubscribeToEventHandler<T, TEventHandler>(T target, TEventHandler handler, Action<T, TEventHandler> subscribe, Action<T, TEventHandler> unsubscribe)
         {
             subscribe(target, handler);

+ 8 - 3
src/Windows/Avalonia.Win32/Automation/AutomationNode.cs

@@ -82,8 +82,8 @@ namespace Avalonia.Win32.Automation
             UiaCoreProviderApi.UiaRaiseStructureChangedEvent(
                 this,
                 StructureChangeType.ChildrenInvalidated,
-                _runtimeId,
-                _runtimeId.Length);
+                null,
+                0);
         }
 
         public void PropertyChanged(AutomationProperty property, object? oldValue, object? newValue) 
@@ -156,6 +156,11 @@ namespace Avalonia.Win32.Automation
                 return null;
             }
 
+            if (Peer.GetType().Name == "PopupAutomationPeer")
+            {
+                System.Diagnostics.Debug.WriteLine("Popup automation node navigate " + direction);
+            }
+
             return InvokeSync(() =>
             {
                 return direction switch
@@ -269,7 +274,7 @@ namespace Avalonia.Win32.Automation
             var peer = Peer;
             var parent = peer.GetParent();
 
-            while (parent is object)
+            while (peer is not AAP.IRootProvider && parent is object)
             {
                 peer = parent;
                 parent = peer.GetParent();

+ 18 - 6
tests/Avalonia.IntegrationTests.Win32/ButtonTests.cs

@@ -9,32 +9,44 @@ namespace Avalonia.IntegrationTests.Win32
         private WindowsDriver<WindowsElement> _session;
         public ButtonTests(TestAppFixture fixture) => _session = fixture.Session;
 
+        [Fact]
+        public void DisabledButton()
+        {
+            SelectTab();
+
+            var button = _session.FindElementByAccessibilityId("DisabledButton");
+
+            Assert.Equal("Disabled Button", button.Text);
+            Assert.False(button.Enabled);
+        }
+
         [Fact]
         public void BasicButton()
         {
-            SelectButtonTab();
+            SelectTab();
             
             var button = _session.FindElementByAccessibilityId("BasicButton");
 
             Assert.Equal("Basic Button", button.Text);
+            Assert.True(button.Enabled);
         }
 
         [Fact]
         public void ButtonWithTextBlock()
         {
-            SelectButtonTab();
+            SelectTab();
 
             var button = _session.FindElementByAccessibilityId("ButtonWithTextBlock");
 
             Assert.Equal("Button with TextBlock", button.Text);
         }
 
-        private WindowsElement SelectButtonTab()
+        private WindowsElement SelectTab()
         {
             var tabs = _session.FindElementByAccessibilityId("MainTabs");
-            var buttonTab = tabs.FindElementByName("Button");
-            buttonTab.Click();
-            return (WindowsElement)buttonTab;
+            var tab = tabs.FindElementByName("Button");
+            tab.Click();
+            return (WindowsElement)tab;
         }
     }
 }

+ 55 - 0
tests/Avalonia.IntegrationTests.Win32/ComboBoxTests.cs

@@ -0,0 +1,55 @@
+using OpenQA.Selenium.Appium.Windows;
+using Xunit;
+
+namespace Avalonia.IntegrationTests.Win32
+{
+    [Collection("IntegrationTestApp collection")]
+    public class ComboBoxTests
+    {
+        private WindowsDriver<WindowsElement> _session;
+        public ComboBoxTests(TestAppFixture fixture) => _session = fixture.Session;
+
+        [Fact]
+        public void UnselectedComboBox()
+        {
+            SelectTab();
+
+            var comboBox = _session.FindElementByAccessibilityId("UnselectedComboBox");
+
+            Assert.Equal(string.Empty, comboBox.Text);
+
+            comboBox.Click();
+            comboBox.FindElementByName("Bar").Click();
+
+            Assert.Equal("Bar", comboBox.Text);
+        }
+
+        [Fact]
+        public void SelectedIndex0ComboBox()
+        {
+            SelectTab();
+
+            var comboBox = _session.FindElementByAccessibilityId("SelectedIndex0ComboBox");
+
+            Assert.Equal("Foo", comboBox.Text);
+        }
+
+        [Fact]
+        public void SelectedIndex1ComboBox()
+        {
+            SelectTab();
+
+            var comboBox = _session.FindElementByAccessibilityId("SelectedIndex1ComboBox");
+
+            Assert.Equal("Bar", comboBox.Text);
+        }
+
+        private WindowsElement SelectTab()
+        {
+            var tabs = _session.FindElementByAccessibilityId("MainTabs");
+            var tab = tabs.FindElementByName("ComboBox");
+            tab.Click();
+            return (WindowsElement)tab;
+        }
+    }
+}