aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMiguel Costa <miguel.costa@qt.io>2019-04-30 17:11:30 +0200
committerMiguel Costa <miguel.costa@qt.io>2019-05-08 10:58:39 +0000
commit6e58bea9883b5ff9c05fb07f807e9566e8911eda (patch)
treee01177701a867e75bcad178e431c7be3284f5a4a
parent9a28ff4fc403d275edf278c4418ef5742a5d33cc (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.cs159
-rw-r--r--src/qtvstest/MacroParser.cs19
-rw-r--r--src/qtvstest/MacroServer.cs2
-rw-r--r--src/qtvstest/QtVsTest.csproj42
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