diff options
author | Miguel Costa <miguel.costa@qt.io> | 2019-04-30 17:11:30 +0200 |
---|---|---|
committer | Miguel Costa <miguel.costa@qt.io> | 2019-05-08 10:58:39 +0000 |
commit | 6e58bea9883b5ff9c05fb07f807e9566e8911eda (patch) | |
tree | e01177701a867e75bcad178e431c7be3284f5a4a | |
parent | 9a28ff4fc403d275edf278c4418ef5742a5d33cc (diff) |
Allow UI automation commands in auto-tests
Auto-test macros can now include commands to manipulate any UI object,
e.g. to automate user interactions required for testing.
Change-Id: I488e581b9f3ecfa16f521893a88be86087556248
Reviewed-by: Oliver Wolff <oliver.wolff@qt.io>
-rw-r--r-- | src/qtvstest/Macro.cs | 159 | ||||
-rw-r--r-- | src/qtvstest/MacroParser.cs | 19 | ||||
-rw-r--r-- | src/qtvstest/MacroServer.cs | 2 | ||||
-rw-r--r-- | src/qtvstest/QtVsTest.csproj | 42 |
4 files changed, 221 insertions, 1 deletions
diff --git a/src/qtvstest/Macro.cs b/src/qtvstest/Macro.cs index 7c8b36f3..b8a3e8b1 100644 --- a/src/qtvstest/Macro.cs +++ b/src/qtvstest/Macro.cs @@ -37,6 +37,7 @@ using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Windows.Automation; using Microsoft.CSharp; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Threading; @@ -103,6 +104,19 @@ namespace QtVsTest.Macros public bool QuitWhenDone { get; private set; } AsyncPackage Package { get; set; } + EnvDTE80.DTE2 Dte { get; set; } + + AutomationElement _UiVsRoot; + AutomationElement UiVsRoot + { + get + { + if (_UiVsRoot == null) + _UiVsRoot = AutomationElement.FromHandle(new IntPtr(Dte.MainWindow.HWnd)); + return _UiVsRoot; + } + } + JoinableTaskFactory JoinableTaskFactory { get; set; } CancellationToken ServerLoop { get; set; } @@ -177,12 +191,14 @@ namespace QtVsTest.Macros /// <param name="serverLoop">Server loop cancellation token</param> public Macro( AsyncPackage package, + EnvDTE80.DTE2 dte, JoinableTaskFactory joinableTaskFactory, CancellationToken serverLoop) { Package = package; JoinableTaskFactory = joinableTaskFactory; ServerLoop = serverLoop; + Dte = dte; ErrorMsg("Uninitialized"); } @@ -267,6 +283,8 @@ namespace QtVsTest.Macros /// <returns></returns> bool CompileMacro() { + if (UiVsRoot == null) + return ErrorMsg("UI Automation not available"); var csharp = new StringBuilder(); @@ -472,10 +490,145 @@ namespace QtVsTest.Macros } break; + + case StatementType.Ui: + if (!GenerateUiStatement(s, csharp)) + return false; + break; } return true; } + public AutomationElement UiFind(AutomationElement uiContext, string[] path) + { + var uiIterator = uiContext; + foreach (var name in path) { + uiIterator = uiIterator.FindFirst(TreeScope.Subtree, + new PropertyCondition(AutomationElement.NameProperty, name)); + if (uiIterator == null) + throw new Exception( + string.Format("Could not find UI element \"{0}\"", name)); + } + return uiIterator; + } + + static readonly IEnumerable<string> UI_TYPES = new[] + { + "Dock", "ExpandCollapse", "GridItem", "Grid", "Invoke", "MultipleView", "RangeValue", + "Scroll", "ScrollItem", "Selection", "SelectionItem", "SynchronizedInput", "Text", + "Transform", "Toggle", "Value", "Window", "VirtualizedItem", "ItemContainer" + }; + + bool GenerateUiGlobals(StringBuilder csharp) + { + csharp.Append(@" + public static Func<AutomationElement, string[], AutomationElement> UiFind; + public static Stack<AutomationElement> UiStack; + public static Dictionary<string, AutomationElement> UiStash; + public static AutomationElement UiVsRoot; + public static AutomationElement UiContext;"); + return false; + } + + bool InitializeUiGlobals() + { + if (MacroClass == null) + return false; + + MacroClass.GetField("UiFind", PUBLIC_STATIC) + .SetValue(null, new Func<AutomationElement, string[], AutomationElement>(UiFind)); + + MacroClass.GetField("UiStack", PUBLIC_STATIC) + .SetValue(null, new Stack<AutomationElement>()); + + MacroClass.GetField("UiStash", PUBLIC_STATIC) + .SetValue(null, new Dictionary<string, AutomationElement>()); + + MacroClass.GetField("UiVsRoot", PUBLIC_STATIC) + .SetValue(null, UiVsRoot); + + MacroClass.GetField("UiContext", PUBLIC_STATIC) + .SetValue(null, UiVsRoot); + + return true; + } + + bool GenerateUiStatement(Statement s, StringBuilder csharp) + { + if (s.Args.Count == 0) + return ErrorMsg("Invalid #ui statement"); + + if (s.Args[0].Equals("context", IGNORE_CASE)) { + //# ui context [ VS ] => _string_ [, _string_, ... ] + //# ui context HWND => _int_ + + if (s.Args.Count > 2 || string.IsNullOrEmpty(s.Code)) + return ErrorMsg("Invalid #ui statement"); + + string context; + if (s.Args.Count == 1) + context = string.Format("UiFind(UiContext, new[] {{ {0} }})", s.Code); + else if (s.Args.Count > 1 && s.Args[1] == "VS") + context = string.Format("UiFind(UiVsRoot, new[] {{ {0} }})", s.Code); + else if (s.Args.Count > 1 && s.Args[1] == "HWND") + context = string.Format("AutomationElement.FromHandle((IntPtr)({0}))", s.Code); + else + return ErrorMsg("Invalid #ui statement"); + + csharp.AppendFormat(@" + UiContext = {0};", context); + + } else if (s.Args[0].Equals("pattern", IGNORE_CASE)) { + //# ui pattern <_TypeName_> <_VarName_> [ => _string_ [, _string_, ... ] ] + //# ui pattern Invoke [ => _string_ [, _string_, ... ] ] + //# ui pattern Toggle [ => _string_ [, _string_, ... ] ] + + if (s.Args.Count < 2) + return ErrorMsg("Invalid #ui statement"); + + string typeName = s.Args[1]; + string varName = (s.Args.Count > 2) ? s.Args[2] : string.Empty; + if (!UI_TYPES.Contains(typeName)) + return ErrorMsg("Invalid #ui statement"); + + string uiElement; + if (!string.IsNullOrEmpty(s.Code)) + uiElement = string.Format("UiFind(UiContext, new[] {{ {0} }})", s.Code); + else + uiElement = "UiContext"; + + string patternTypeId = string.Format("{0}PatternIdentifiers.Pattern", typeName); + string patternType = string.Format("{0}Pattern", typeName); + + if (!string.IsNullOrEmpty(varName)) { + + csharp.AppendFormat(@" + var {0} = {1}.GetCurrentPattern({2}) as {3};", + varName, + uiElement, + patternTypeId, + patternType); + + } else if (typeName == "Invoke" || typeName == "Toggle") { + + csharp.AppendFormat(@" + ({0}.GetCurrentPattern({1}) as {2}).{3}();", + uiElement, + patternTypeId, + patternType, + typeName); + + } else { + return ErrorMsg("Invalid #ui statement"); + } + + } else { + return ErrorMsg("Invalid #ui statement"); + } + + return true; + } + const string SERVICETYPE_PREFIX = "_ServiceType_"; const string INIT_PREFIX = "_Init_"; string MethodName { get { return string.Format("_Run_{0}_Async", Name); } } @@ -537,6 +690,9 @@ namespace QtVsTest.Macros if (!GenerateResultFuncs(csharp)) return false; + if (!GenerateUiGlobals(csharp)) + return false; + csharp.AppendFormat( /** BEGIN generate code **/ @" @@ -627,6 +783,9 @@ namespace QtVsTest.Macros MacroClass.GetField("WaitExpr", PUBLIC_STATIC) .SetValue(null, new Func<int, Func<object>, Task>(WaitExprAsync)); + if (!InitializeUiGlobals()) + return false; + return NoError(); } diff --git a/src/qtvstest/MacroParser.cs b/src/qtvstest/MacroParser.cs index 29c3f3bc..8864e541 100644 --- a/src/qtvstest/MacroParser.cs +++ b/src/qtvstest/MacroParser.cs @@ -87,6 +87,25 @@ namespace QtVsTest.Macros //# wait [timeout] [ <var type> <var name> ] => <expr> Wait, + // UI automation command + // + // Set context based on UI element name path + //# ui context [ VS ] => _string_ [, _string_, ... ] + // + // Set context based on window handle + //# ui context HWND => _int_ + // + // Get reference to UI element pattern. By default, the current context is used as source. + // A name path relative to the current context allows using a child element as source. + //# ui pattern <_TypeName_> <_VarName_> [ => _string_ [, _string_, ... ] ] + // + // Get reference to UI element Invoke pattern and immediately call the Invoke() method. + //# ui pattern Invoke [ => _string_ [, _string_, ... ] ] + // + // Get reference to UI element Toggle pattern and immediately call the Toggle() method. + //# ui pattern Toggle [ => _string_ [, _string_, ... ] ] + Ui, + // Close Visual Studio //# quit Quit diff --git a/src/qtvstest/MacroServer.cs b/src/qtvstest/MacroServer.cs index d2f179a4..a7341845 100644 --- a/src/qtvstest/MacroServer.cs +++ b/src/qtvstest/MacroServer.cs @@ -94,7 +94,7 @@ namespace QtVsTest.Macros if (Loop.Token.IsCancellationRequested) break; - var macro = new Macro(Package, JoinableTaskFactory, Loop.Token); + var macro = new Macro(Package, DTE, JoinableTaskFactory, Loop.Token); await macro.CompileAsync(Encoding.UTF8.GetString(data)); if (macro.AutoRun) await macro.RunAsync(); diff --git a/src/qtvstest/QtVsTest.csproj b/src/qtvstest/QtVsTest.csproj index af5b2d0b..063ad75d 100644 --- a/src/qtvstest/QtVsTest.csproj +++ b/src/qtvstest/QtVsTest.csproj @@ -67,6 +67,7 @@ <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <PlatformTarget>x86</PlatformTarget> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> @@ -117,4 +118,45 @@ namespace MSBuild.MetaInfo { <Name>QtVsTools.RegExpr</Name> </ProjectReference> </ItemGroup> + + <Choose> + <When Condition="Exists('$(MSBuildProgramFiles32)\Windows Kits\10\bin\10.0.17763.0')"> + <PropertyGroup> + <Win10SDKPath>$(MSBuildProgramFiles32)\Windows Kits\10\bin\10.0.17763.0</Win10SDKPath> + </PropertyGroup> + </When> + <When Condition="Exists('$(MSBuildProgramFiles32)\Windows Kits\10\bin\10.0.17134.0')"> + <PropertyGroup> + <Win10SDKPath>$(MSBuildProgramFiles32)\Windows Kits\10\bin\10.0.17134.0</Win10SDKPath> + </PropertyGroup> + </When> + <When Condition="Exists('$(MSBuildProgramFiles32)\Windows Kits\10\bin\10.0.16299.15')"> + <PropertyGroup> + <Win10SDKPath>$(MSBuildProgramFiles32)\Windows Kits\10\bin\10.0.16299.15</Win10SDKPath> + </PropertyGroup> + </When> + <When Condition="Exists('$(MSBuildProgramFiles32)\Windows Kits\10\bin\10.0.15063.0')"> + <PropertyGroup> + <Win10SDKPath>$(MSBuildProgramFiles32)\Windows Kits\10\bin\10.0.15063.0</Win10SDKPath> + </PropertyGroup> + </When> + </Choose> + + <PropertyGroup Condition="'$(Win10SDKPath)' != ''"> + <UIAVerifyPath>$(Win10SDKPath)\x86\UIAVerify</UIAVerifyPath> + </PropertyGroup> + + <ItemGroup Condition="'$(UIAVerifyPath)' != ''"> + <Reference Include="Interop.UIAutomationClient"> + <HintPath>$(UIAVerifyPath)\Interop.UIAutomationClient.dll</HintPath> + <SpecificVersion>False</SpecificVersion> + <EmbedInteropTypes>False</EmbedInteropTypes> + </Reference> + <Reference Include="UIAComWrapper"> + <HintPath>$(UIAVerifyPath)\UIAComWrapper.dll</HintPath> + <Private>True</Private> + <Aliases>global</Aliases> + <EmbedInteropTypes>False</EmbedInteropTypes> + </Reference> + </ItemGroup> </Project>
\ No newline at end of file |