From 451f3156abdc8882defe85f95f18e2435a4edbca Mon Sep 17 00:00:00 2001 From: Ben Weedon Date: Mon, 17 May 2021 13:54:02 -0700 Subject: [PATCH] Add IsOpcodeSupported support to WinRT and C++ wrappers (#78) IsOpcodeSupported is an API provided by CoreAutomationRemoteOperation which determines whether a given remote operation opcode is supported by the current provider connection. Which opcodes are supported depends on both the client and provider UIA versions. This change simply plumbs IsOpcodeSupported through Microsoft.UI.UIAutomation and UiaOperationAbstraction. A more elegant design taking advantage of the characteristics of each wrapper can be made in the future. For UiaOperationAbstraction, we always return true from IsOpcodeSupported in the local case. Note, IsOpcodeSupported will throw E_FAIL on future versions of Windows when no connection has yet been established. Once such versions of Windows are released, we should update tests and comments with the specifics. This is tracked by issue #77. --- .../UiaOperationAbstractionTests.cpp | 78 ++++++++++++++++++- .../FunctionalTests/WinRTBuilderTests.cpp | 33 ++++++++ .../AutomationRemoteOperation.cpp | 5 ++ .../AutomationRemoteOperation.h | 7 ++ .../Microsoft.UI.UIAutomation.idl | 2 + .../UiaOperationAbstraction.cpp | 14 ++++ .../UiaOperationAbstraction.h | 11 +++ 7 files changed, 149 insertions(+), 1 deletion(-) diff --git a/src/UIAutomation/FunctionalTests/UiaOperationAbstractionTests.cpp b/src/UIAutomation/FunctionalTests/UiaOperationAbstractionTests.cpp index 1c424e0..cd952c5 100644 --- a/src/UIAutomation/FunctionalTests/UiaOperationAbstractionTests.cpp +++ b/src/UIAutomation/FunctionalTests/UiaOperationAbstractionTests.cpp @@ -2240,5 +2240,81 @@ namespace UiaOperationAbstractionTests { TryBlockTest(false); } + + // Test that calling IsOpcodeSupported works after we import an + // element, establishing a connection. + void IsOpcodeSupportedTest_AfterImport(const bool useRemoteOperations) + { + ModernApp app(L"Microsoft.WindowsCalculator_8wekyb3d8bbwe!App"); + app.Activate(); + auto calc = WaitForElementFocus(L"Display is 0"); + UiaElement element = calc; + + auto guard = InitializeUiaOperationAbstraction(useRemoteOperations); + + auto scope = UiaOperationScope::StartNew(); + scope.BindInput(element); + + Assert::IsTrue(scope.IsOpcodeSupported(0x0 /*Nop*/)); + Assert::IsTrue(scope.IsOpcodeSupported(0x48 /*NewGuid*/)); + + const auto isNonExistentOpcodeSupported = scope.IsOpcodeSupported(10000000000000); + if (useRemoteOperations) + { + Assert::IsFalse(isNonExistentOpcodeSupported); + } + else + { + // All operations are supported locally. + Assert::IsTrue(isNonExistentOpcodeSupported); + } + } + + TEST_METHOD(IsOpcodeSupported_AfterImport_Remote) + { + IsOpcodeSupportedTest_AfterImport(true); + } + + TEST_METHOD(IsOpcodeSupported_AfterImport_Local) + { + IsOpcodeSupportedTest_AfterImport(false); + } + + // Test that calling IsOpcodeSupported fails for remote ops on certain + // Windows builds if we haven't imported an element yet, thus not + // establishing a connection. + void IsOpcodeSupportedTest_BeforeImport(const bool useRemoteOperations) + { + auto guard = InitializeUiaOperationAbstraction(useRemoteOperations); + + auto scope = UiaOperationScope::StartNew(); + + const auto hr = wil::ResultFromException([&]() + { + scope.IsOpcodeSupported(0x0); + }); + + if (useRemoteOperations) + { + // TODO #77: Do a Windows version check once a build with this + // failing behavior is released. + // Assert::AreEqual(E_FAIL, hr); + } + else + { + // Calling IsOpcodeSupported never produces an error in the local case. + Assert::AreEqual(S_OK, hr); + } + } + + TEST_METHOD(IsOpcodeSupported_BeforeImport_Remote) + { + IsOpcodeSupportedTest_BeforeImport(true); + } + + TEST_METHOD(IsOpcodeSupported_BeforeImport_Local) + { + IsOpcodeSupportedTest_BeforeImport(false); + } }; -} \ No newline at end of file +} diff --git a/src/UIAutomation/FunctionalTests/WinRTBuilderTests.cpp b/src/UIAutomation/FunctionalTests/WinRTBuilderTests.cpp index bcb37d3..a153240 100644 --- a/src/UIAutomation/FunctionalTests/WinRTBuilderTests.cpp +++ b/src/UIAutomation/FunctionalTests/WinRTBuilderTests.cpp @@ -520,5 +520,38 @@ namespace WinRTBuilderTests Assert::AreEqual(true, winrt::unbox_value(results.GetResult(isNullToken))); Assert::IsTrue(results.GetResult(metadataToken) == nullptr); } + + // Test that calling IsOpcodeSupported works after we import an + // element, establishing a connection. + TEST_METHOD(IsOpcodeSupported_AfterImport) + { + ModernApp app(L"Microsoft.WindowsCalculator_8wekyb3d8bbwe!App"); + app.Activate(); + auto calc = WaitForElementFocus(L"Display is 0"); + + winrt::AutomationRemoteOperation op; + auto remoteElement = op.ImportElement(calc.as()); + + Assert::IsTrue(op.IsOpcodeSupported(0x0 /*Nop*/)); + Assert::IsTrue(op.IsOpcodeSupported(0x48 /*NewGuid*/)); + Assert::IsFalse(op.IsOpcodeSupported(10000000000000)); + } + + // Test that calling IsOpcodeSupported fails on certain Windows builds + // if we haven't imported an element yet, thus not establishing a + // connection. + TEST_METHOD(IsOpcodeSupported_BeforeImport) + { + winrt::AutomationRemoteOperation op; + + const auto hr = wil::ResultFromException([&]() + { + op.IsOpcodeSupported(0x0); + }); + + // TODO #77: Do a Windows version check once a build with this + // failing behavior is released. + // Assert::AreEqual(E_FAIL, hr); + } }; } diff --git a/src/UIAutomation/Microsoft.UI.UIAutomation/AutomationRemoteOperation.cpp b/src/UIAutomation/Microsoft.UI.UIAutomation/AutomationRemoteOperation.cpp index 1c642c8..b1ba419 100644 --- a/src/UIAutomation/Microsoft.UI.UIAutomation/AutomationRemoteOperation.cpp +++ b/src/UIAutomation/Microsoft.UI.UIAutomation/AutomationRemoteOperation.cpp @@ -152,6 +152,11 @@ namespace winrt::Microsoft::UI::UIAutomation::implementation return make(newId, *this); } + bool AutomationRemoteOperation::IsOpcodeSupported(const uint32_t opcode) const + { + return m_remoteOperation.IsOpcodeSupported(opcode); + } + winrt::AutomationRemoteElement AutomationRemoteOperation::ImportElement(winrt::AutomationElement const& element) { const auto elementId = GetNextId(); diff --git a/src/UIAutomation/Microsoft.UI.UIAutomation/AutomationRemoteOperation.h b/src/UIAutomation/Microsoft.UI.UIAutomation/AutomationRemoteOperation.h index 6520629..c30d9ec 100644 --- a/src/UIAutomation/Microsoft.UI.UIAutomation/AutomationRemoteOperation.h +++ b/src/UIAutomation/Microsoft.UI.UIAutomation/AutomationRemoteOperation.h @@ -67,6 +67,13 @@ namespace winrt::Microsoft::UI::UIAutomation::implementation winrt::AutomationRemoteStringMap NewStringMap(); winrt::AutomationRemoteAnyObject NewNull(); + // Returns whether the given opcode is supported in the current remote + // operation connection. Calls directly into the corresponding + // CoreAutomationRemoteOperation API. Throws E_FAIL if no connection is + // currently active. + // TODO #77: Specify which Windows release we start throwing that error. + bool IsOpcodeSupported(uint32_t opcode) const; + winrt::AutomationRemoteElement ImportElement(winrt::Windows::UI::UIAutomation::AutomationElement const& element); winrt::AutomationRemoteTextRange ImportTextRange(winrt::Windows::UI::UIAutomation::AutomationTextRange const& textRange); diff --git a/src/UIAutomation/Microsoft.UI.UIAutomation/Microsoft.UI.UIAutomation.idl b/src/UIAutomation/Microsoft.UI.UIAutomation/Microsoft.UI.UIAutomation.idl index 16de3ef..dc44d31 100644 --- a/src/UIAutomation/Microsoft.UI.UIAutomation/Microsoft.UI.UIAutomation.idl +++ b/src/UIAutomation/Microsoft.UI.UIAutomation/Microsoft.UI.UIAutomation.idl @@ -1760,6 +1760,8 @@ namespace Microsoft.UI.UIAutomation AutomationRemoteStringMap NewStringMap(); AutomationRemoteAnyObject NewNull(); + Boolean IsOpcodeSupported(UInt32 opcode); + AutomationRemoteElement ImportElement(Windows.UI.UIAutomation.AutomationElement element); AutomationRemoteTextRange ImportTextRange(Windows.UI.UIAutomation.AutomationTextRange textRange); diff --git a/src/UIAutomation/UiaOperationAbstraction/UiaOperationAbstraction.cpp b/src/UIAutomation/UiaOperationAbstraction/UiaOperationAbstraction.cpp index 5a78773..6feb550 100644 --- a/src/UIAutomation/UiaOperationAbstraction/UiaOperationAbstraction.cpp +++ b/src/UIAutomation/UiaOperationAbstraction/UiaOperationAbstraction.cpp @@ -790,6 +790,20 @@ namespace UiaOperationAbstraction return m_useRemoteApi; } + bool UiaOperationDelegator::IsOpcodeSupported(const uint32_t opcode) const + { + if (m_useRemoteApi) + { + return m_remoteOperation.IsOpcodeSupported(opcode); + } + else + { + // If we're not in a remote operation we'll just be using classic + // UIA, in which everything is supported. + return true; + } + } + void UiaOperationDelegator::AbortOperationWithHresult(HRESULT hr) { if (m_useRemoteApi && m_remoteOperation) diff --git a/src/UIAutomation/UiaOperationAbstraction/UiaOperationAbstraction.h b/src/UIAutomation/UiaOperationAbstraction/UiaOperationAbstraction.h index fd32e00..db8f1a5 100644 --- a/src/UIAutomation/UiaOperationAbstraction/UiaOperationAbstraction.h +++ b/src/UIAutomation/UiaOperationAbstraction/UiaOperationAbstraction.h @@ -124,6 +124,12 @@ namespace UiaOperationAbstraction bool GetUseRemoteApi() const; + // Returns whether the given opcode is supported in the current remote + // operation connection if remote, returns true if local. Throws E_FAIL + // if remote and no connection is currently active. + // TODO #77: Specify which Windows release we start throwing that error. + bool IsOpcodeSupported(uint32_t opcode) const; + template void If(UiaBool conditionBool, OnTrue&& onTrue, OnFalse&& onFalse) const { @@ -2430,6 +2436,11 @@ namespace UiaOperationAbstraction return GetCurrentDelegator()->GetUseRemoteApi(); } + bool IsOpcodeSupported(const uint32_t opcode) const + { + return GetCurrentDelegator()->IsOpcodeSupported(opcode); + } + template inline void If(UiaBool conditionBool, OnTrue&& onTrue, OnFalse&& onFalse) const {