From 4d410b5a7cb9343aa2569c4085b9955a9b001d79 Mon Sep 17 00:00:00 2001 From: David Kean Date: Fri, 28 Aug 2026 20:35:47 +1000 Subject: [PATCH] Restore UIA support for custom controls System.Windows.Automation replaces the Windows MSAA proxy with its own client-side provider. That provider ignores richer patterns, properties, and property-change events exposed through IAccessibleEx. Compose server support before legacy inference, preserve existing fallbacks, and bridge ExpandCollapse and Toggle state events. Retain valid Tree selection contracts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../LibraryAssemblyInfo.cs | 7 + .../Internal/AutomationProxies/Accessible.cs | 106 ++++ .../AutomationProxies/MSAAEventDispatcher.cs | 27 +- .../AutomationProxies/MSAANativeProvider.cs | 43 +- .../AutomationProxies/MSAAWinEventWrap.cs | 30 +- .../MS/Win32/NativeMethods.cs | 1 + .../MS/Win32/UnsafeNativeMethods.cs | 24 +- .../UIAutomationClientSideProviders.csproj | 1 + .../AutomationProxies/AccessibleExTests.cs | 503 ++++++++++++++++++ .../PresentationCore.Tests.csproj | 7 + 10 files changed, 735 insertions(+), 14 deletions(-) create mode 100644 src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/LibraryAssemblyInfo.cs create mode 100644 src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/MS/Internal/AutomationProxies/AccessibleExTests.cs diff --git a/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/LibraryAssemblyInfo.cs b/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/LibraryAssemblyInfo.cs new file mode 100644 index 00000000000..56d27d4d336 --- /dev/null +++ b/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/LibraryAssemblyInfo.cs @@ -0,0 +1,7 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Microsoft.Internal; + +[assembly: InternalsVisibleTo($"PresentationCore.Tests, PublicKey={BuildInfo.WCP_PUBLIC_KEY_STRING}")] diff --git a/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Internal/AutomationProxies/Accessible.cs b/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Internal/AutomationProxies/Accessible.cs index 20f10608ad2..f5405b26705 100644 --- a/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Internal/AutomationProxies/Accessible.cs +++ b/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Internal/AutomationProxies/Accessible.cs @@ -9,6 +9,7 @@ using System.Diagnostics; using System.Globalization; using System.Windows.Automation; +using System.Windows.Automation.Provider; using System.Windows; using Accessibility; using System.Runtime.InteropServices; @@ -295,6 +296,82 @@ internal static Accessible Wrap(IAccessible acc, int idChild) internal bool IsFocused { get { return HasState(AccessibleState.Focused); } } internal bool IsOffScreen { get { return HasState(AccessibleState.Offscreen); } } + internal object GetPatternProvider(int patternId) + { + return GetPatternProvider(AccessibleExProvider, patternId); + } + + internal object GetPropertyValue(int propertyId) + { + return GetPropertyValue(AccessibleExProvider, propertyId); + } + + internal static object GetPatternProvider(IRawElementProviderSimple provider, int patternId) + { + if (provider == null) + { + return null; + } + + try + { + return provider.GetPatternProvider(patternId); + } + catch (Exception e) when (IsAccessibleExUnavailable(e)) + { + return null; + } + } + + internal static object GetPropertyValue(IRawElementProviderSimple provider, int propertyId) + { + if (provider == null) + { + return null; + } + + try + { + return provider.GetPropertyValue(propertyId); + } + catch (Exception e) when (IsAccessibleExUnavailable(e)) + { + return null; + } + } + + internal static IRawElementProviderSimple GetAccessibleExProvider(UnsafeNativeMethods.IServiceProvider serviceProvider, int childId) + { + if (serviceProvider == null) + { + return null; + } + + try + { + Guid serviceId = typeof(UnsafeNativeMethods.IAccessibleEx).GUID; + Guid interfaceId = serviceId; + UnsafeNativeMethods.IAccessibleEx accessibleEx = + serviceProvider.QueryService(ref serviceId, ref interfaceId) as UnsafeNativeMethods.IAccessibleEx; + + if (accessibleEx == null) + { + return null; + } + + if (childId != NativeMethods.CHILD_SELF) + { + accessibleEx = accessibleEx.GetObjectForChild(childId); + } + + return accessibleEx as IRawElementProviderSimple; + } + catch (Exception e) when (IsAccessibleExUnavailable(e)) + { + return null; + } + } + internal Accessible FirstChild { get @@ -1391,6 +1468,19 @@ private static bool HandleIAccessibleException(Exception e) return true; } + private static bool IsAccessibleExUnavailable(Exception e) + { + COMException comException = e as COMException; + return e is ArgumentException + || e is InvalidCastException + || e is NotImplementedException + || comException != null + && (comException.ErrorCode == NativeMethods.E_FAIL + || comException.ErrorCode == NativeMethods.E_NOINTERFACE + || comException.ErrorCode == NativeMethods.E_NOTIMPL + || comException.ErrorCode == NativeMethods.E_INVALIDARG); + } + // IAccessibles that we get from Winforms apps in partial trust return failure // code for some methods - notably accNavigate and accChild. The operation will // succeed, however, if we first navigate up to the parent, and then back down @@ -1476,9 +1566,25 @@ private enum NavDir private IAccessible _acc; // a full IAccessible object or an IAccessible parent that is managing a ChildID private int _idChild; // this is ChildID which is the ID a server gives this child (not related to child order!) private int _accessibleChildrenIndex; // this is how many children to skip over when calling AccessibleChildren + private bool _accessibleExProviderInitialized; + private IRawElementProviderSimple _accessibleExProvider; private IntPtr _hwnd; + private IRawElementProviderSimple AccessibleExProvider + { + get + { + if (!_accessibleExProviderInitialized) + { + _accessibleExProvider = GetAccessibleExProvider(_acc as UnsafeNativeMethods.IServiceProvider, _idChild); + _accessibleExProviderInitialized = true; + } + + return _accessibleExProvider; + } + } + #endregion Private Fields } } diff --git a/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Internal/AutomationProxies/MSAAEventDispatcher.cs b/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Internal/AutomationProxies/MSAAEventDispatcher.cs index 068f94a1cc6..689005d5af2 100644 --- a/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Internal/AutomationProxies/MSAAEventDispatcher.cs +++ b/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Internal/AutomationProxies/MSAAEventDispatcher.cs @@ -24,7 +24,13 @@ internal class MSAAEventDispatcher : MSAAWinEventWrap #region Constructors private MSAAEventDispatcher() - : base(NativeMethods.EVENT_OBJECT_CREATE, NativeMethods.EVENT_OBJECT_ACCELERATORCHANGE) + : base( + NativeMethods.EVENT_OBJECT_CREATE, + NativeMethods.EVENT_OBJECT_ACCELERATORCHANGE, + ExpandCollapsePattern.ExpandCollapseStateProperty.Id, + ExpandCollapsePattern.ExpandCollapseStateProperty.Id, + TogglePattern.ToggleStateProperty.Id, + TogglePattern.ToggleStateProperty.Id) { } #endregion Constructors @@ -179,6 +185,13 @@ internal override void WinEventProc(int eventId, IntPtr hwnd, int idObject, int // get the 2-nd level table of events and properties we are listening for in this window Hashtable eventTable = (Hashtable)_hwndTable[hwnd]; + AutomationProperty property = GetPatternPropertyFromWinEvent(eventId); + if (property != null) + { + MaybeFirePropertyChangeEvent(null, property, eventTable, hwnd, idObject, idChild, true); + return; + } + switch (eventId) { case NativeMethods.EVENT_OBJECT_CREATE: @@ -242,6 +255,18 @@ internal override void WinEventProc(int eventId, IntPtr hwnd, int idObject, int // break; } } + + } + + internal static AutomationProperty GetPatternPropertyFromWinEvent(int eventId) + { + if (eventId == ExpandCollapsePattern.ExpandCollapseStateProperty.Id) + return ExpandCollapsePattern.ExpandCollapseStateProperty; + + if (eventId == TogglePattern.ToggleStateProperty.Id) + return TogglePattern.ToggleStateProperty; + + return null; } #endregion Internal Methods diff --git a/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Internal/AutomationProxies/MSAANativeProvider.cs b/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Internal/AutomationProxies/MSAANativeProvider.cs index 01933f705a4..bc2383a7af2 100644 --- a/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Internal/AutomationProxies/MSAANativeProvider.cs +++ b/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Internal/AutomationProxies/MSAANativeProvider.cs @@ -486,6 +486,10 @@ ProviderOptions IRawElementProviderSimple.ProviderOptions object IRawElementProviderSimple.GetPatternProvider(int patternId) { + object provider = _acc.GetPatternProvider(patternId); + if (provider != null) + return provider; + AutomationPattern pattern = AutomationPattern.LookupById(patternId); //Debug.WriteLine.WriteLine(string.Format(CultureInfo.CurrentCulture, "{0} IRawElementProviderSimple.GetPatternProvider {1}", this, pattern)); @@ -495,6 +499,10 @@ object IRawElementProviderSimple.GetPatternProvider(int patternId) object IRawElementProviderSimple.GetPropertyValue(int propertyId) { + object value = _acc.GetPropertyValue(propertyId); + if (value != null) + return value; + AutomationProperty idProp = AutomationProperty.LookupById(propertyId); //Debug.WriteLine.WriteLine(string.Format(CultureInfo.CurrentCulture, "{0} IRawElementProviderSimple.GetPropertyValue {1}", this, idProp)); @@ -669,7 +677,13 @@ IRawElementProviderSimple ISelectionItemProvider.SelectionContainer { //Debug.WriteLine.WriteLine(string.Format(CultureInfo.CurrentCulture, "{0} ISelectionItemProvider.SelectionContainer", this)); - return IsRoot ? null : Parent; + MsaaNativeProvider parent = IsRoot ? null : Parent; + while (parent != null && !parent.IsPatternSupported(SelectionPattern.Pattern)) + { + parent = parent.IsRoot ? null : parent.Parent; + } + + return parent; } } #endregion ISelectionItemProvider @@ -880,6 +894,10 @@ protected virtual object GetPatternProvider(AutomationPattern pattern) // overridable implementation of IRawElementProviderSimple.GetPropertyValue protected virtual object GetPropertyValue(AutomationProperty idProp) { + object patternValue = GetPatternPropertyValue(this, idProp); + if (patternValue != null) + return patternValue; + // The following UIA properties need support: AcceleratorKeyProperty, AccessKeyProperty, AutomationIdProperty, // HasKeyboardFocusProperty, IsContentElementProperty, IsControlElementProperty, IsKeyboardFocusableProperty, // IsPasswordProperty, IsReadOnlyProperty, NativeObjectModelAccessProperty, SiblingIdProperty, TabIndexProperty?, @@ -907,6 +925,7 @@ protected virtual object GetPropertyValue(AutomationProperty idProp) else return null; } + else if (idProp == AutomationElement.IsEnabledProperty) { return _acc.IsEnabled; @@ -953,6 +972,24 @@ protected virtual object GetPropertyValue(AutomationProperty idProp) return null; } + internal static object GetPatternPropertyValue(IRawElementProviderSimple provider, AutomationProperty property) + { + if (property == ExpandCollapsePattern.ExpandCollapseStateProperty) + { + IExpandCollapseProvider expandCollapse = + provider.GetPatternProvider(ExpandCollapsePattern.Pattern.Id) as IExpandCollapseProvider; + return expandCollapse?.ExpandCollapseState; + } + + if (property == TogglePattern.ToggleStateProperty) + { + IToggleProvider toggle = provider.GetPatternProvider(TogglePattern.Pattern.Id) as IToggleProvider; + return toggle?.ToggleState; + } + + return null; + } + // overridable method used by value pattern to retrieve the value. protected virtual string GetValue() { @@ -1309,7 +1346,9 @@ public CtrlTypePatterns(ControlType ctrlType, params AutomationPattern[] pattern new CtrlTypePatterns(ControlType.RadioButton, SelectionItemPattern.Pattern), // ControlType.Slider: it is impossible to tell which of RangeValue or Selection patterns to support so we're not supporting either. // ControlType.Spinner: it is impossible to tell which of RangeValue or Selection patterns to support so we're not supporting either. - new CtrlTypePatterns(ControlType.SplitButton, InvokePattern.Pattern) + new CtrlTypePatterns(ControlType.SplitButton, InvokePattern.Pattern), + new CtrlTypePatterns(ControlType.Tree, SelectionPattern.Pattern), + new CtrlTypePatterns(ControlType.TreeItem, SelectionItemPattern.Pattern) }; private Accessible _acc; // the IAccessible we are representing. use Accessible to access. diff --git a/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Internal/AutomationProxies/MSAAWinEventWrap.cs b/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Internal/AutomationProxies/MSAAWinEventWrap.cs index 4c315912ad1..f25d75fe9e3 100644 --- a/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Internal/AutomationProxies/MSAAWinEventWrap.cs +++ b/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Internal/AutomationProxies/MSAAWinEventWrap.cs @@ -27,12 +27,14 @@ internal class MSAAWinEventWrap #region Constructors - // ctor that takes a range of events - internal MSAAWinEventWrap(int eventMin, int eventMax) + // ctor that takes one or more event ranges as min/max pairs + internal MSAAWinEventWrap(params int[] eventRanges) { - _eventMin = eventMin; - _eventMax = eventMax; - _hHooks = new IntPtr[1]; + if (eventRanges == null || eventRanges.Length == 0 || eventRanges.Length % 2 != 0) + throw new ArgumentException(SR.InvalidParameter); + + _eventRanges = (int[])eventRanges.Clone(); + _hHooks = new IntPtr[eventRanges.Length / 2]; Init(); } @@ -74,12 +76,21 @@ internal void StartListening() { _fBusy = true; + for (int i = 0; i < _hHooks.Length; i++) { - // in a single hook, listen for a range of WinEvent types - _hHooks[0] = Misc.SetWinEventHook(_eventMin, _eventMax, IntPtr.Zero, _winEventProc, 0, 0, _fFlags); - if (_hHooks[0] == IntPtr.Zero) + int rangeIndex = i * 2; + _hHooks[i] = Misc.SetWinEventHook( + _eventRanges[rangeIndex], + _eventRanges[rangeIndex + 1], + IntPtr.Zero, + _winEventProc, + 0, + 0, + _fFlags); + if (_hHooks[i] == IntPtr.Zero) { StopListening(); + return; } } _fBusy = false; @@ -213,8 +224,7 @@ internal WinEvent(int eventId, IntPtr hwnd, int idObject, int idChild) } private Queue _qEvents; // Queue of events waiting to be processed - private int _eventMin; // minimum WinEvent type in range - private int _eventMax; // maximium WinEventType in range + private int[] _eventRanges; // pairs of minimum and maximum WinEvent types private IntPtr [] _hHooks; // the returned handles(s) from SetWinEventHook private bool _fBusy; // Flag indicating if we're busy processing private int _fFlags; // SetWinEventHook flags diff --git a/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Win32/NativeMethods.cs b/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Win32/NativeMethods.cs index f627535fad4..1b257479017 100644 --- a/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Win32/NativeMethods.cs +++ b/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Win32/NativeMethods.cs @@ -1140,6 +1140,7 @@ internal static int LODWORD(long n) internal const int E_ACCESSDENIED = unchecked((int)0x80070005); internal const int E_FAIL = unchecked((int)0x80004005); + internal const int E_NOINTERFACE = unchecked((int)0x80004002); internal const int E_UNEXPECTED = unchecked((int)0x8000FFFF); internal const int E_INVALIDARG = unchecked((int)0x80070057); internal const int E_MEMBERNOTFOUND = unchecked((int)0x80020003); diff --git a/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Win32/UnsafeNativeMethods.cs b/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Win32/UnsafeNativeMethods.cs index 02d9126dcdf..9732ba4cae8 100644 --- a/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Win32/UnsafeNativeMethods.cs +++ b/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/MS/Win32/UnsafeNativeMethods.cs @@ -8,6 +8,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Diagnostics; +using System.Windows.Automation.Provider; using NativeMethodsSetLastError = MS.Internal.UIAutomationClientSideProviders.NativeMethodsSetLastError; namespace MS.Win32 @@ -79,6 +80,28 @@ internal static class UnsafeNativeMethods internal static Guid IID_IDispatch = new Guid(0x00020400, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); internal static Guid IID_IAccessible = new Guid(0x618736e0, 0x3c3d, 0x11cf, 0x81, 0x0c, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71); + [ComImport, Guid("6D5140C1-7436-11CE-8034-00AA006009FA"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IServiceProvider + { + [return: MarshalAs(UnmanagedType.IUnknown)] + object QueryService(ref Guid service, ref Guid riid); + } + + [ComImport, Guid("F8B80ADA-2C44-48D0-89BE-5FF23C9CD875"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IAccessibleEx + { + [return: MarshalAs(UnmanagedType.Interface)] + IAccessibleEx GetObjectForChild(int idChild); + + void GetIAccessiblePair([MarshalAs(UnmanagedType.Interface)] out IAccessible accessible, out int childId); + + [return: MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_I4)] + int[] GetRuntimeId(); + + [return: MarshalAs(UnmanagedType.Interface)] + IAccessibleEx ConvertReturnedElement([MarshalAs(UnmanagedType.Interface)] IRawElementProviderSimple provider); + } + [DllImport("oleacc.dll", SetLastError=true)] internal static extern IntPtr GetProcessHandleFromHwnd(IntPtr hwnd); @@ -474,4 +497,3 @@ public struct LHITTESTINFO } } } - diff --git a/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/UIAutomationClientSideProviders.csproj b/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/UIAutomationClientSideProviders.csproj index 106e50cb575..a8ae860c8c1 100644 --- a/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/UIAutomationClientSideProviders.csproj +++ b/src/Microsoft.DotNet.Wpf/src/UIAutomation/UIAutomationClientSideProviders/UIAutomationClientSideProviders.csproj @@ -15,6 +15,7 @@ + Common\System\SR.cs diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/MS/Internal/AutomationProxies/AccessibleExTests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/MS/Internal/AutomationProxies/AccessibleExTests.cs new file mode 100644 index 00000000000..d92d0484587 --- /dev/null +++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/MS/Internal/AutomationProxies/AccessibleExTests.cs @@ -0,0 +1,503 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +extern alias uiaProviders; + +using System.Runtime.InteropServices; +using System.Windows.Automation; +using System.Windows.Automation.Provider; +using Accessibility; +using Moq; +using Accessible = uiaProviders::MS.Internal.AutomationProxies.Accessible; +using AccessibleRole = uiaProviders::MS.Internal.AutomationProxies.AccessibleRole; +using MSAAEventDispatcher = uiaProviders::MS.Internal.AutomationProxies.MSAAEventDispatcher; +using MsaaNativeProvider = uiaProviders::MS.Internal.AutomationProxies.MsaaNativeProvider; +using NativeMethods = uiaProviders::MS.Win32.NativeMethods; +using UnsafeNativeMethods = uiaProviders::MS.Win32.UnsafeNativeMethods; + +namespace PresentationCore.Tests.MS.Internal.AutomationProxies; + +public class AccessibleExTests +{ + [Fact] + public void IsPatternSupported_OutlineItem_SupportsSelectionItem() + { + TestMsaaNativeProvider provider = new(CreateAccessible(AccessibleRole.OutlineItem)); + + Assert.True(provider.IsPatternSupported(SelectionItemPattern.Pattern)); + } + + [Fact] + public void IsPatternSupported_Outline_SupportsSelection() + { + TestMsaaNativeProvider provider = new(CreateAccessible(AccessibleRole.Outline)); + + Assert.True(provider.IsPatternSupported(SelectionPattern.Pattern)); + } + + [Fact] + public void SelectionContainer_NestedOutlineItem_ReturnsOutlineAncestor() + { + TestMsaaNativeProvider root = new( + CreateAccessible(AccessibleRole.Outline), + parent: null, + knownRoot: null, + MsaaNativeProvider.RootStatus.Root); + TestMsaaNativeProvider parent = new( + CreateAccessible(AccessibleRole.OutlineItem), + root, + root, + MsaaNativeProvider.RootStatus.NotRoot); + TestMsaaNativeProvider child = new( + CreateAccessible(AccessibleRole.OutlineItem), + parent, + root, + MsaaNativeProvider.RootStatus.NotRoot); + + IRawElementProviderSimple? container = + ((ISelectionItemProvider)child).SelectionContainer; + + Assert.Same(root, container); + } + + [Fact] + public void GetPatternProvider_UnknownPattern_ReturnsNull() + { + TestMsaaNativeProvider provider = new(CreateAccessible(AccessibleRole.OutlineItem)); + + object? pattern = + ((IRawElementProviderSimple)provider).GetPatternProvider(int.MaxValue); + + Assert.Null(pattern); + } + + [Fact] + public void GetAccessibleExProvider_Self_ReturnsRootProvider() + { + TestAccessibleEx accessibleEx = new(); + TestServiceProvider serviceProvider = new(accessibleEx); + + IRawElementProviderSimple? provider = + Accessible.GetAccessibleExProvider(serviceProvider, NativeMethods.CHILD_SELF); + + Assert.Same(accessibleEx, provider); + Assert.Equal(typeof(UnsafeNativeMethods.IAccessibleEx).GUID, serviceProvider.ServiceId); + Assert.Equal(typeof(UnsafeNativeMethods.IAccessibleEx).GUID, serviceProvider.InterfaceId); + Assert.Equal(0, accessibleEx.GetObjectForChildCallCount); + } + + [Fact] + public void GetAccessibleExProvider_Child_ReturnsChildProvider() + { + TestAccessibleEx child = new(); + TestAccessibleEx root = new() { Child = child }; + TestServiceProvider serviceProvider = new(root); + + IRawElementProviderSimple? provider = + Accessible.GetAccessibleExProvider(serviceProvider, childId: 42); + + Assert.Same(child, provider); + Assert.Equal(1, root.GetObjectForChildCallCount); + Assert.Equal(42, root.LastChildId); + } + + [Fact] + public void GetAccessibleExProvider_ServiceDoesNotImplementIAccessibleEx_ReturnsNull() + { + TestServiceProvider serviceProvider = new(new object()); + + IRawElementProviderSimple? provider = + Accessible.GetAccessibleExProvider(serviceProvider, NativeMethods.CHILD_SELF); + + Assert.Null(provider); + } + + [Theory] + [InlineData(NativeMethods.E_FAIL)] + [InlineData(NativeMethods.E_NOINTERFACE)] + [InlineData(NativeMethods.E_NOTIMPL)] + [InlineData(NativeMethods.E_INVALIDARG)] + public void GetAccessibleExProvider_UnsupportedService_ReturnsNull(int errorCode) + { + TestServiceProvider serviceProvider = new(GetExceptionForHR(errorCode)); + + IRawElementProviderSimple? provider = + Accessible.GetAccessibleExProvider(serviceProvider, NativeMethods.CHILD_SELF); + + Assert.Null(provider); + } + + [Fact] + public void GetAccessibleExProvider_UnsupportedChild_ReturnsNull() + { + TestAccessibleEx root = new() + { + GetObjectForChildException = GetExceptionForHR(NativeMethods.E_INVALIDARG) + }; + + IRawElementProviderSimple? provider = + Accessible.GetAccessibleExProvider(new TestServiceProvider(root), childId: 42); + + Assert.Null(provider); + } + + [Fact] + public void GetAccessibleExProvider_UnexpectedComFailure_Throws() + { + COMException expected = Assert.IsType( + GetExceptionForHR(NativeMethods.RPC_E_DISCONNECTED)); + TestServiceProvider serviceProvider = new(expected); + + COMException actual = Assert.Throws( + () => Accessible.GetAccessibleExProvider(serviceProvider, NativeMethods.CHILD_SELF)); + + Assert.Same(expected, actual); + } + + [Fact] + public void GetPatternProvider_SupportedPattern_ReturnsPatternProvider() + { + object expected = new(); + TestAccessibleEx accessibleEx = new() { PatternProvider = expected }; + + object? actual = Accessible.GetPatternProvider(accessibleEx, patternId: 10005); + + Assert.Same(expected, actual); + } + + [Theory] + [InlineData(NativeMethods.E_FAIL)] + [InlineData(NativeMethods.E_NOTIMPL)] + [InlineData(NativeMethods.E_INVALIDARG)] + public void GetPatternProvider_UnsupportedPattern_ReturnsNull(int errorCode) + { + TestAccessibleEx accessibleEx = new() + { + GetPatternProviderException = GetExceptionForHR(errorCode) + }; + + object? actual = Accessible.GetPatternProvider(accessibleEx, patternId: 10005); + + Assert.Null(actual); + } + + [Fact] + public void GetPatternProvider_UnexpectedComFailure_Throws() + { + COMException expected = Assert.IsType( + GetExceptionForHR(NativeMethods.RPC_E_DISCONNECTED)); + TestAccessibleEx accessibleEx = new() { GetPatternProviderException = expected }; + + COMException actual = Assert.Throws( + () => Accessible.GetPatternProvider(accessibleEx, patternId: 10005)); + + Assert.Same(expected, actual); + } + + [Fact] + public void GetPatternProvider_ProjectedInvalidArgument_ReturnsNull() + { + Exception projected = GetExceptionForHR(NativeMethods.E_INVALIDARG); + Assert.IsType(projected); + TestAccessibleEx accessibleEx = new() + { + GetPatternProviderException = projected + }; + + object? actual = Accessible.GetPatternProvider(accessibleEx, patternId: 10005); + + Assert.Null(actual); + } + + [Fact] + public void GetPatternProvider_ProjectedNoInterface_ReturnsNull() + { + Exception projected = GetExceptionForHR(NativeMethods.E_NOINTERFACE); + Assert.IsType(projected); + TestAccessibleEx accessibleEx = new() + { + GetPatternProviderException = projected + }; + + object? actual = Accessible.GetPatternProvider(accessibleEx, patternId: 10005); + + Assert.Null(actual); + } + + [Fact] + public void GetPropertyValue_SupportedProperty_ReturnsValue() + { + object expected = new(); + TestAccessibleEx accessibleEx = new() { PropertyValue = expected }; + + object? actual = Accessible.GetPropertyValue(accessibleEx, propertyId: 30152); + + Assert.Same(expected, actual); + } + + [Theory] + [InlineData(NativeMethods.E_FAIL)] + [InlineData(NativeMethods.E_NOINTERFACE)] + [InlineData(NativeMethods.E_NOTIMPL)] + [InlineData(NativeMethods.E_INVALIDARG)] + public void GetPropertyValue_UnsupportedProperty_ReturnsNull(int errorCode) + { + TestAccessibleEx accessibleEx = new() + { + GetPropertyValueException = GetExceptionForHR(errorCode) + }; + + object? actual = Accessible.GetPropertyValue(accessibleEx, propertyId: 30152); + + Assert.Null(actual); + } + + [Fact] + public void GetPropertyValue_UnexpectedComFailure_Throws() + { + COMException expected = Assert.IsType( + GetExceptionForHR(NativeMethods.RPC_E_DISCONNECTED)); + TestAccessibleEx accessibleEx = new() { GetPropertyValueException = expected }; + + COMException actual = Assert.Throws( + () => Accessible.GetPropertyValue(accessibleEx, propertyId: 30152)); + + Assert.Same(expected, actual); + } + + [Fact] + public void GetPatternPropertyValue_ExpandCollapse_ReturnsState() + { + TestAccessibleEx provider = new() + { + PatternProvider = new TestExpandCollapseProvider(ExpandCollapseState.Expanded) + }; + + object? actual = MsaaNativeProvider.GetPatternPropertyValue( + provider, + ExpandCollapsePattern.ExpandCollapseStateProperty); + + Assert.Equal(ExpandCollapseState.Expanded, actual); + } + + [Fact] + public void GetPatternPropertyValue_Toggle_ReturnsState() + { + TestAccessibleEx provider = new() + { + PatternProvider = new TestToggleProvider(ToggleState.Indeterminate) + }; + + object? actual = MsaaNativeProvider.GetPatternPropertyValue( + provider, + TogglePattern.ToggleStateProperty); + + Assert.Equal(ToggleState.Indeterminate, actual); + } + + [Fact] + public void GetPatternPropertyValue_UnrelatedProperty_ReturnsNull() + { + object? actual = MsaaNativeProvider.GetPatternPropertyValue( + new TestAccessibleEx(), + AutomationElement.NameProperty); + + Assert.Null(actual); + } + + [Fact] + public void GetPatternPropertyFromWinEvent_ExpandCollapse_ReturnsProperty() + { + AutomationProperty? property = MSAAEventDispatcher.GetPatternPropertyFromWinEvent( + ExpandCollapsePattern.ExpandCollapseStateProperty.Id); + + Assert.Same(ExpandCollapsePattern.ExpandCollapseStateProperty, property); + } + + [Fact] + public void GetPatternPropertyFromWinEvent_Toggle_ReturnsProperty() + { + AutomationProperty? property = MSAAEventDispatcher.GetPatternPropertyFromWinEvent( + TogglePattern.ToggleStateProperty.Id); + + Assert.Same(TogglePattern.ToggleStateProperty, property); + } + + [Theory] + [InlineData(NativeMethods.EVENT_OBJECT_STATECHANGE)] + [InlineData(30000)] + public void GetPatternPropertyFromWinEvent_UnsupportedProperty_ReturnsNull(int eventId) + { + Assert.Null(MSAAEventDispatcher.GetPatternPropertyFromWinEvent(eventId)); + } + + private static Exception GetExceptionForHR(int errorCode) + { + return Marshal.GetExceptionForHR(errorCode)!; + } + + private sealed class TestServiceProvider : UnsafeNativeMethods.IServiceProvider + { + private readonly object _service; + + internal TestServiceProvider(object service) + { + _service = service; + } + + internal Guid ServiceId { get; private set; } + internal Guid InterfaceId { get; private set; } + + public object QueryService(ref Guid service, ref Guid riid) + { + ServiceId = service; + InterfaceId = riid; + + if (_service is Exception exception) + { + throw exception; + } + + return _service; + } + } + + private sealed class TestAccessibleEx : + UnsafeNativeMethods.IAccessibleEx, + IRawElementProviderSimple + { + internal TestAccessibleEx? Child { get; set; } + internal Exception? GetObjectForChildException { get; set; } + internal Exception? GetPatternProviderException { get; set; } + internal Exception? GetPropertyValueException { get; set; } + internal object? PatternProvider { get; set; } + internal object? PropertyValue { get; set; } + internal int GetObjectForChildCallCount { get; private set; } + internal int LastChildId { get; private set; } + + public UnsafeNativeMethods.IAccessibleEx GetObjectForChild(int idChild) + { + GetObjectForChildCallCount++; + LastChildId = idChild; + + if (GetObjectForChildException is Exception exception) + { + throw exception; + } + + return Child!; + } + + public void GetIAccessiblePair(out IAccessible accessible, out int childId) + { + throw new NotSupportedException(); + } + + public int[] GetRuntimeId() + { + throw new NotSupportedException(); + } + + public UnsafeNativeMethods.IAccessibleEx ConvertReturnedElement(IRawElementProviderSimple provider) + { + throw new NotSupportedException(); + } + + ProviderOptions IRawElementProviderSimple.ProviderOptions => + ProviderOptions.ServerSideProvider; + + object IRawElementProviderSimple.GetPatternProvider(int patternId) + { + if (GetPatternProviderException is Exception exception) + { + throw exception; + } + + return PatternProvider!; + } + + object IRawElementProviderSimple.GetPropertyValue(int propertyId) + { + if (GetPropertyValueException is Exception exception) + { + throw exception; + } + + return PropertyValue!; + } + + IRawElementProviderSimple IRawElementProviderSimple.HostRawElementProvider => null!; + } + + private sealed class TestExpandCollapseProvider : IExpandCollapseProvider + { + internal TestExpandCollapseProvider(ExpandCollapseState state) + { + ExpandCollapseState = state; + } + + public ExpandCollapseState ExpandCollapseState { get; } + + public void Collapse() + { + throw new NotSupportedException(); + } + + public void Expand() + { + throw new NotSupportedException(); + } + } + + private sealed class TestToggleProvider : IToggleProvider + { + internal TestToggleProvider(ToggleState state) + { + ToggleState = state; + } + + public ToggleState ToggleState { get; } + + public void Toggle() + { + throw new NotSupportedException(); + } + } + + private static Accessible CreateAccessible(AccessibleRole role) + { + Mock accessible = new(MockBehavior.Strict); + accessible + .Setup(instance => instance.get_accRole(NativeMethods.CHILD_SELF)) + .Returns((int)role); + return Accessible.Wrap(accessible.Object); + } + + private sealed class TestMsaaNativeProvider : MsaaNativeProvider + { + internal TestMsaaNativeProvider(Accessible accessible) + : this( + accessible, + parent: null, + knownRoot: null, + RootStatus.NotRoot) + { + } + + internal TestMsaaNativeProvider( + Accessible accessible, + MsaaNativeProvider? parent, + MsaaNativeProvider? knownRoot, + RootStatus rootStatus) + : base( + accessible, + new IntPtr(1), + parent!, + knownRoot!, + rootStatus) + { + } + } +} diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/PresentationCore.Tests.csproj b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/PresentationCore.Tests.csproj index 43d961d246a..cead242a717 100644 --- a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/PresentationCore.Tests.csproj +++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/PresentationCore.Tests.csproj @@ -23,9 +23,16 @@ + + + uiaProviders + + + +