Lots of refactoring
This commit is contained in:
Родитель
5987467cf5
Коммит
9db6ca7f87
|
@ -7,14 +7,13 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.Maui.Hosting;
|
using Microsoft.Maui.Hosting;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using Microsoft.Maui.Automation.Remote;
|
|
||||||
using Grpc.Core;
|
using Grpc.Core;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Automation
|
namespace Microsoft.Maui.Automation
|
||||||
{
|
{
|
||||||
public static class AutomationAppBuilderExtensions
|
public static class AutomationAppBuilderExtensions
|
||||||
{
|
{
|
||||||
static GrpcRemoteAppHost host;
|
static GrpcRemoteAppAgent client;
|
||||||
|
|
||||||
static IApplication CreateApp(
|
static IApplication CreateApp(
|
||||||
Maui.IApplication app
|
Maui.IApplication app
|
||||||
|
@ -42,10 +41,8 @@ namespace Microsoft.Maui.Automation
|
||||||
return multiApp;
|
return multiApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void StartAutomationServiceListener(this Maui.IApplication mauiApplication, int port = 10882)
|
public static void StartAutomationServiceListener(this Maui.IApplication mauiApplication, string address)
|
||||||
{
|
{
|
||||||
var address = IPAddress.Any;
|
|
||||||
|
|
||||||
var multiApp = CreateApp(mauiApplication
|
var multiApp = CreateApp(mauiApplication
|
||||||
#if ANDROID
|
#if ANDROID
|
||||||
, (Android.App.Application.Context as Android.App.Application)
|
, (Android.App.Application.Context as Android.App.Application)
|
||||||
|
@ -53,15 +50,8 @@ namespace Microsoft.Maui.Automation
|
||||||
#endif
|
#endif
|
||||||
);
|
);
|
||||||
|
|
||||||
host = new GrpcRemoteAppHost(multiApp);
|
client = new GrpcRemoteAppAgent(multiApp, address);
|
||||||
|
|
||||||
var server = new Server
|
|
||||||
{
|
|
||||||
Services = { RemoteGrpc.RemoteApp.BindService(host) },
|
|
||||||
Ports = { new ServerPort(address.ToString(), port, ServerCredentials.Insecure) }
|
|
||||||
};
|
|
||||||
|
|
||||||
server.Start();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,14 +13,12 @@ namespace Microsoft.Maui.Automation
|
||||||
{
|
{
|
||||||
public override Platform DefaultPlatform => Platform.Ios;
|
public override Platform DefaultPlatform => Platform.Ios;
|
||||||
|
|
||||||
public override Task<string> GetProperty(Platform platform, string elementId, string propertyName)
|
public override async Task<string> GetProperty(Platform platform, string elementId, string propertyName)
|
||||||
{
|
{
|
||||||
var selector = new ObjCRuntime.Selector(propertyName);
|
var selector = new ObjCRuntime.Selector(propertyName);
|
||||||
var getSelector = new ObjCRuntime.Selector("get" + System.Globalization.CultureInfo.InvariantCulture.TextInfo.ToTitleCase(propertyName));
|
var getSelector = new ObjCRuntime.Selector("get" + System.Globalization.CultureInfo.InvariantCulture.TextInfo.ToTitleCase(propertyName));
|
||||||
|
|
||||||
var roots = GetRootElements();
|
var element = (await FindElements(platform, e => e.Id?.Equals(elementId) ?? false))?.FirstOrDefault();
|
||||||
|
|
||||||
var element = roots.FindDepthFirst(new IdSelector(elementId))?.FirstOrDefault();
|
|
||||||
|
|
||||||
if (element is not null && element.PlatformElement is NSObject nsobj)
|
if (element is not null && element.PlatformElement is NSObject nsobj)
|
||||||
{
|
{
|
||||||
|
@ -28,32 +26,29 @@ namespace Microsoft.Maui.Automation
|
||||||
{
|
{
|
||||||
var v = nsobj.PerformSelector(selector)?.ToString();
|
var v = nsobj.PerformSelector(selector)?.ToString();
|
||||||
if (v != null)
|
if (v != null)
|
||||||
return Task.FromResult(v);
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nsobj.RespondsToSelector(getSelector))
|
if (nsobj.RespondsToSelector(getSelector))
|
||||||
{
|
{
|
||||||
var v = nsobj.PerformSelector(getSelector)?.ToString();
|
var v = nsobj.PerformSelector(getSelector)?.ToString();
|
||||||
if (v != null)
|
if (v != null)
|
||||||
return Task.FromResult(v);
|
return v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult<string>(string.Empty);
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<IEnumerable<Element>> GetElements(Platform platform, string elementId = null, int depth = 0)
|
public override Task<IEnumerable<Element>> GetElements(Platform platform)
|
||||||
{
|
{
|
||||||
var root = GetRootElements();
|
var root = GetRootElements(-1);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(elementId))
|
return Task.FromResult(root);
|
||||||
return Task.FromResult(root);
|
|
||||||
|
|
||||||
return Task.FromResult(root.FindDepthFirst(new IdSelector(elementId)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
IEnumerable<Element> GetRootElements()
|
IEnumerable<Element> GetRootElements(int depth)
|
||||||
{
|
{
|
||||||
var children = new List<Element>();
|
var children = new List<Element>();
|
||||||
|
|
||||||
|
@ -69,7 +64,7 @@ namespace Microsoft.Maui.Automation
|
||||||
{
|
{
|
||||||
foreach (var window in windowScene.Windows)
|
foreach (var window in windowScene.Windows)
|
||||||
{
|
{
|
||||||
children.Add(window.GetElement(this));
|
children.Add(window.GetElement(this, 1, depth));
|
||||||
hadScenes = true;
|
hadScenes = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,13 +78,39 @@ namespace Microsoft.Maui.Automation
|
||||||
{
|
{
|
||||||
foreach (var window in UIApplication.SharedApplication.Windows)
|
foreach (var window in UIApplication.SharedApplication.Windows)
|
||||||
{
|
{
|
||||||
children.Add(window.GetElement(this));
|
children.Add(window.GetElement(this, 1, depth));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override Task<IEnumerable<Element>> FindElements(Platform platform, Func<Element, bool> matcher)
|
||||||
|
{
|
||||||
|
var windows = GetRootElements(-1);
|
||||||
|
|
||||||
|
var matches = new List<Element>();
|
||||||
|
Traverse(platform, windows, matches, matcher);
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<Element>>(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Traverse(Platform platform, IEnumerable<Element> elements, IList<Element> matches, Func<Element, bool> matcher)
|
||||||
|
{
|
||||||
|
foreach (var e in elements)
|
||||||
|
{
|
||||||
|
if (matcher(e))
|
||||||
|
matches.Add(e);
|
||||||
|
|
||||||
|
if (e.PlatformElement is UIView uiView)
|
||||||
|
{
|
||||||
|
var children = uiView.Subviews?.Select(s => s.GetElement(this, e.Id, 1, 1))
|
||||||
|
?.ToList() ?? new List<Element>();
|
||||||
|
Traverse(platform, children, matches, matcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
|
@ -49,7 +49,7 @@ internal static class iOSExtensions
|
||||||
return ti.TextInRange(range);
|
return ti.TextInRange(range);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Element GetElement(this UIKit.UIView uiView, IApplication application, string parentId = "")
|
public static Element GetElement(this UIKit.UIView uiView, IApplication application, string parentId = "", int currentDepth = -1, int maxDepth = -1)
|
||||||
{
|
{
|
||||||
var e = new Element(application, Platform.Ios, uiView.Handle.ToString(), uiView, parentId)
|
var e = new Element(application, Platform.Ios, uiView.Handle.ToString(), uiView, parentId)
|
||||||
{
|
{
|
||||||
|
@ -66,13 +66,17 @@ internal static class iOSExtensions
|
||||||
Text = uiView.GetText()
|
Text = uiView.GetText()
|
||||||
};
|
};
|
||||||
|
|
||||||
var children = uiView.Subviews?.Select(s => s.GetElement(application, e.Id))?.ToList<Element>() ?? new List<Element>();
|
if (maxDepth <= 0 || (currentDepth + 1 <= maxDepth))
|
||||||
|
{
|
||||||
|
var children = uiView.Subviews?.Select(s => s.GetElement(application, e.Id, currentDepth + 1, maxDepth))
|
||||||
|
?.ToList() ?? new List<Element>();
|
||||||
|
|
||||||
e.Children.AddRange(children);
|
e.Children.AddRange(children);
|
||||||
|
}
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Element GetElement(this UIWindow window, IApplication application)
|
public static Element GetElement(this UIWindow window, IApplication application, int currentDepth = -1, int maxDepth = -1)
|
||||||
{
|
{
|
||||||
var e = new Element(application, Platform.Ios, window.Handle.ToString(), window)
|
var e = new Element(application, Platform.Ios, window.Handle.ToString(), window)
|
||||||
{
|
{
|
||||||
|
@ -82,9 +86,12 @@ internal static class iOSExtensions
|
||||||
Text = string.Empty
|
Text = string.Empty
|
||||||
};
|
};
|
||||||
|
|
||||||
var children = window.Subviews?.Select(s => s.GetElement(application, e.Id))?.ToList<Element>() ?? new List<Element>();
|
if (maxDepth <= 0 || (currentDepth + 1 <= maxDepth))
|
||||||
|
{
|
||||||
|
var children = window.Subviews?.Select(s => s.GetElement(application, e.Id, currentDepth + 1, maxDepth))?.ToList() ?? new List<Element>();
|
||||||
|
|
||||||
e.Children.AddRange(children);
|
e.Children.AddRange(children);
|
||||||
|
}
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
using Grpc.Core;
|
|
||||||
using Microsoft.Maui.Automation.RemoteGrpc;
|
|
||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.Maui.Automation.Remote
|
|
||||||
{
|
|
||||||
public class GrpcRemoteAppHost : RemoteGrpc.RemoteApp.RemoteAppBase
|
|
||||||
{
|
|
||||||
public GrpcRemoteAppHost(IApplication application)
|
|
||||||
{
|
|
||||||
this.Application = application;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected readonly IApplication Application;
|
|
||||||
|
|
||||||
public async override Task<ElementsResponse> GetElements(ElementsRequest request, ServerCallContext context)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var elements = await Application.GetElements(request.Platform, request.ElementId, request.ChildDepth);
|
|
||||||
|
|
||||||
var resp = new ElementsResponse();
|
|
||||||
resp.Elements.AddRange(elements);
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -47,80 +47,4 @@ namespace Microsoft.Maui.Automation
|
||||||
throw new PlatformNotSupportedException();
|
throw new PlatformNotSupportedException();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class HelperExtensions
|
|
||||||
{
|
|
||||||
internal static void PushAllReverse<T>(this Stack<T> st, IEnumerable<T> elems)
|
|
||||||
{
|
|
||||||
foreach (var elem in elems.Reverse())
|
|
||||||
st.Push(elem);
|
|
||||||
}
|
|
||||||
|
|
||||||
//internal static IEnumerable<Element> FindDepthFirst(this IEnumerable<Element> elements, IElementSelector? selector)
|
|
||||||
//{
|
|
||||||
// var list = new List<Element>();
|
|
||||||
// foreach (var e in elements)
|
|
||||||
// list.Add(e);
|
|
||||||
|
|
||||||
// foreach (var e in FindDepthFirst(list, selector))
|
|
||||||
// yield return e;
|
|
||||||
//}
|
|
||||||
|
|
||||||
internal static IEnumerable<Element> FindDepthFirst(this IEnumerable<Element> elements, IElementSelector? selector)
|
|
||||||
{
|
|
||||||
var st = new Stack<Element>();
|
|
||||||
st.PushAllReverse(elements);
|
|
||||||
|
|
||||||
while (st.Count > 0)
|
|
||||||
{
|
|
||||||
var v = st.Pop();
|
|
||||||
if (selector == null || selector.Matches(v))
|
|
||||||
{
|
|
||||||
yield return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
st.PushAllReverse(v.Children);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//internal static IEnumerable<Element> FindBreadthFirst(this IEnumerable<Element> elements, IElementSelector? selector)
|
|
||||||
//{
|
|
||||||
// var list = new List<Element>();
|
|
||||||
// foreach (var e in elements)
|
|
||||||
// list.Add(e);
|
|
||||||
|
|
||||||
// foreach (var e in FindBreadthFirst(list, selector))
|
|
||||||
// yield return e;
|
|
||||||
//}
|
|
||||||
|
|
||||||
internal static IEnumerable<Element> FindBreadthFirst(this IEnumerable<Element> elements, IElementSelector? selector)
|
|
||||||
{
|
|
||||||
var q = new Queue<Element>();
|
|
||||||
|
|
||||||
foreach (var e in elements)
|
|
||||||
q.Enqueue(e);
|
|
||||||
|
|
||||||
while (q.Count > 0)
|
|
||||||
{
|
|
||||||
var v = q.Dequeue();
|
|
||||||
if (selector == null || selector.Matches(v))
|
|
||||||
{
|
|
||||||
yield return v;
|
|
||||||
}
|
|
||||||
foreach (var c in v.Children)
|
|
||||||
q.Enqueue(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IReadOnlyCollection<T> ToReadOnlyCollection<T>(this IEnumerable<T> elems)
|
|
||||||
{
|
|
||||||
return new ReadOnlyCollection<T>(elems.ToList());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IReadOnlyCollection<Element> AsReadOnlyCollection(this Element element)
|
|
||||||
{
|
|
||||||
var list = new List<Element> { element };
|
|
||||||
return list.AsReadOnly();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Maui.Automation.RemoteGrpc;
|
||||||
|
using Microsoft.Maui.Dispatching;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
@ -16,48 +18,66 @@ namespace Microsoft.Maui.Automation
|
||||||
?? App.GetCurrentMauiApplication() ?? throw new PlatformNotSupportedException();
|
?? App.GetCurrentMauiApplication() ?? throw new PlatformNotSupportedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
Task<TResult> Dispatch<TResult>(Func<TResult> action)
|
Task<TResult> Dispatch<TResult>(Func<Task<TResult>> action)
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<TResult>();
|
|
||||||
|
|
||||||
var dispatcher = MauiPlatformApplication.Handler.MauiContext.Services.GetService<Dispatching.IDispatcher>() ?? throw new Exception("Unable to locate Dispatcher");
|
var dispatcher = MauiPlatformApplication.Handler.MauiContext.Services.GetService<Dispatching.IDispatcher>() ?? throw new Exception("Unable to locate Dispatcher");
|
||||||
|
|
||||||
dispatcher.Dispatch(() =>
|
return dispatcher.DispatchAsync(action);
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var r = action();
|
|
||||||
tcs.TrySetResult(r);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
tcs.TrySetException(ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return tcs.Task;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Platform DefaultPlatform => Platform.Maui;
|
public override Platform DefaultPlatform => Platform.Maui;
|
||||||
|
|
||||||
public readonly Maui.IApplication MauiPlatformApplication;
|
public readonly Maui.IApplication MauiPlatformApplication;
|
||||||
|
|
||||||
public override async Task<IEnumerable<Element>> GetElements(Platform platform, string elementId = null, int depth = 0)
|
public override Task<IEnumerable<Element>> GetElements(Platform platform)
|
||||||
{
|
=> Dispatch<IEnumerable<Element>>(() =>
|
||||||
var windows = await Dispatch(() =>
|
|
||||||
{
|
{
|
||||||
var result = new List<Element>();
|
var windows = new List<Element>();
|
||||||
|
|
||||||
foreach (var window in MauiPlatformApplication.Windows)
|
foreach (var window in MauiPlatformApplication.Windows)
|
||||||
{
|
{
|
||||||
var w = window.GetMauiElement(this);
|
var w = window.GetMauiElement(this, currentDepth: 1, maxDepth: -1);
|
||||||
result.Add(w);
|
windows.Add(w);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return Task.FromResult<IEnumerable<Element>>(windows);
|
||||||
});
|
});
|
||||||
|
|
||||||
return windows;
|
public override Task<IEnumerable<Element>> FindElements(Platform platform, Func<Element, bool> matcher)
|
||||||
|
=> Dispatch<IEnumerable<Element>>(() =>
|
||||||
|
{
|
||||||
|
var windows = new List<Element>();
|
||||||
|
|
||||||
|
foreach (var window in MauiPlatformApplication.Windows)
|
||||||
|
{
|
||||||
|
var w = window.GetMauiElement(this, currentDepth: 1, maxDepth: 1);
|
||||||
|
windows.Add(w);
|
||||||
|
}
|
||||||
|
|
||||||
|
var matches = new List<Element>();
|
||||||
|
Traverse(platform, windows, matches, matcher);
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<Element>>(matches);
|
||||||
|
});
|
||||||
|
|
||||||
|
void Traverse(Platform platform, IEnumerable<Element> elements, IList<Element> matches, Func<Element, bool> matcher)
|
||||||
|
{
|
||||||
|
foreach (var e in elements)
|
||||||
|
{
|
||||||
|
if (matcher(e))
|
||||||
|
matches.Add(e);
|
||||||
|
|
||||||
|
if (e.PlatformElement is IView view)
|
||||||
|
{
|
||||||
|
var children = view.GetChildren(this, e.Id, 1, 1);
|
||||||
|
Traverse(platform, children, matches, matcher);
|
||||||
|
}
|
||||||
|
else if (e.PlatformElement is IWindow window)
|
||||||
|
{
|
||||||
|
var children = window.GetChildren(this, e.Id, 1, 1);
|
||||||
|
Traverse(platform, children, matches, matcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<string> GetProperty(Platform platform, string elementId, string propertyName)
|
public override Task<string> GetProperty(Platform platform, string elementId, string propertyName)
|
||||||
|
|
|
@ -1,21 +1,22 @@
|
||||||
using Microsoft.Maui.Controls;
|
using Microsoft.Maui.Controls;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using static System.Net.Mime.MediaTypeNames;
|
using static System.Net.Mime.MediaTypeNames;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Automation
|
namespace Microsoft.Maui.Automation
|
||||||
{
|
{
|
||||||
internal static class MauiExtensions
|
internal static class MauiExtensions
|
||||||
{
|
{
|
||||||
internal static Element[] GetChildren(this Maui.IWindow window, IApplication application, string parentId = "")
|
internal static Element[] GetChildren(this Maui.IWindow window, IApplication application, string parentId = "", int currentDepth = -1, int maxDepth = -1)
|
||||||
{
|
{
|
||||||
if (window.Content == null)
|
if (window.Content == null)
|
||||||
return Array.Empty<Element>();
|
return Array.Empty<Element>();
|
||||||
|
|
||||||
return new[] { window.Content.ToMauiAutomationView(application, parentId) };
|
return new[] { window.Content.GetMauiElement(application, parentId, currentDepth, maxDepth) };
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static Element[] GetChildren(this Maui.IView view, IApplication application, string parentId = "")
|
internal static Element[] GetChildren(this Maui.IView view, IApplication application, string parentId = "", int currentDepth = -1, int maxDepth = -1)
|
||||||
{
|
{
|
||||||
if (view is ILayout layout)
|
if (view is ILayout layout)
|
||||||
{
|
{
|
||||||
|
@ -23,57 +24,57 @@ namespace Microsoft.Maui.Automation
|
||||||
|
|
||||||
foreach (var v in layout)
|
foreach (var v in layout)
|
||||||
{
|
{
|
||||||
children.Add(v.ToMauiAutomationView(application, parentId));
|
children.Add(v.GetMauiElement(application, parentId, currentDepth, maxDepth));
|
||||||
}
|
}
|
||||||
|
|
||||||
return children.ToArray();
|
return children.ToArray();
|
||||||
}
|
}
|
||||||
else if (view is IContentView content && content?.Content is Maui.IView contentView)
|
else if (view is IContentView content && content?.Content is Maui.IView contentView)
|
||||||
{
|
{
|
||||||
return new[] { contentView.ToMauiAutomationView(application, parentId) };
|
return new[] { contentView.GetMauiElement(application, parentId, currentDepth, maxDepth) };
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.Empty<Element>();
|
return Array.Empty<Element>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
internal static Element ToPlatformAutomationWindow(this Maui.IWindow window, IApplication application)
|
internal static Element GetPlatformElement(this Maui.IWindow window, IApplication application, int currentDepth = -1, int maxDepth = -1)
|
||||||
{
|
{
|
||||||
#if ANDROID
|
#if ANDROID
|
||||||
if (window.Handler.PlatformView is Android.App.Activity activity)
|
if (window.Handler.PlatformView is Android.App.Activity activity)
|
||||||
return activity.GetElement(application);
|
return activity.GetElement(application, currentDepth, maxDepth);
|
||||||
#elif IOS || MACCATALYST
|
#elif IOS || MACCATALYST
|
||||||
if (window.Handler.PlatformView is UIKit.UIWindow uiwindow)
|
if (window.Handler.PlatformView is UIKit.UIWindow uiwindow)
|
||||||
return uiwindow.GetElement(application);
|
return uiwindow.GetElement(application, currentDepth, maxDepth);
|
||||||
#elif WINDOWS
|
#elif WINDOWS
|
||||||
if (window.Handler.PlatformView is Microsoft.UI.Xaml.Window xamlwindow)
|
if (window.Handler.PlatformView is Microsoft.UI.Xaml.Window xamlwindow)
|
||||||
return xamlwindow.GetElement(application);
|
return xamlwindow.GetElement(application, currentDepth, maxDepth);
|
||||||
#endif
|
#endif
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static Element GetPlatformElement(this Maui.IView view, IApplication application, string parentId = "")
|
internal static Element GetPlatformElement(this Maui.IView view, IApplication application, string parentId = "", int currentDepth = -1, int maxDepth = -1)
|
||||||
{
|
{
|
||||||
#if ANDROID
|
#if ANDROID
|
||||||
if (view.Handler.PlatformView is Android.Views.View androidview)
|
if (view.Handler.PlatformView is Android.Views.View androidview)
|
||||||
return androidview.GetElement(application, parentId);
|
return androidview.GetElement(application, parentId, currentDepth, maxDepth);
|
||||||
#elif IOS || MACCATALYST
|
#elif IOS || MACCATALYST
|
||||||
if (view.Handler.PlatformView is UIKit.UIView uiview)
|
if (view.Handler.PlatformView is UIKit.UIView uiview)
|
||||||
return uiview.GetElement(application, parentId);
|
return uiview.GetElement(application, parentId, currentDepth, maxDepth);
|
||||||
#elif WINDOWS
|
#elif WINDOWS
|
||||||
if (view.Handler.PlatformView is Microsoft.UI.Xaml.UIElement uielement)
|
if (view.Handler.PlatformView is Microsoft.UI.Xaml.UIElement uielement)
|
||||||
return uielement.GetElement(application, parentId);
|
return uielement.GetElement(application, parentId, currentDepth, maxDepth);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
internal static Element GetMauiElement(this Maui.IWindow window, IApplication application)
|
internal static Element GetMauiElement(this Maui.IWindow window, IApplication application, string parentId = "", int currentDepth = -1, int maxDepth = -1)
|
||||||
{
|
{
|
||||||
var platformElement = window.ToPlatformAutomationWindow(application);
|
var platformElement = window.GetPlatformElement(application);
|
||||||
|
|
||||||
var e = new Element(application, Platform.Maui, platformElement.Id, platformElement)
|
var e = new Element(application, Platform.Maui, platformElement.Id, window, parentId)
|
||||||
{
|
{
|
||||||
Id = platformElement.Id,
|
Id = platformElement.Id,
|
||||||
AutomationId = platformElement.AutomationId ?? platformElement.Id,
|
AutomationId = platformElement.AutomationId ?? platformElement.Id,
|
||||||
|
@ -84,18 +85,18 @@ namespace Microsoft.Maui.Automation
|
||||||
Text = platformElement.Text ?? ""
|
Text = platformElement.Text ?? ""
|
||||||
};
|
};
|
||||||
|
|
||||||
e.Children.AddRange(window.GetChildren(application, e.Id));
|
if (maxDepth <= 0 || (currentDepth + 1 <= maxDepth))
|
||||||
|
e.Children.AddRange(window.GetChildren(application, e.Id, currentDepth + 1, maxDepth));
|
||||||
|
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static Element ToMauiAutomationView(this Maui.IView view, IApplication application, string parentId = "")
|
internal static Element GetMauiElement(this Maui.IView view, IApplication application, string parentId = "", int currentDepth = -1, int maxDepth = -1)
|
||||||
{
|
{
|
||||||
var platformElement = view.GetPlatformElement(application, parentId);
|
var platformElement = view.GetPlatformElement(application, parentId, currentDepth, maxDepth);
|
||||||
|
|
||||||
var e = new Element(application, Platform.Maui, platformElement.Id, parentId)
|
var e = new Element(application, Platform.Maui, platformElement.Id, view, parentId)
|
||||||
{
|
{
|
||||||
PlatformElement = platformElement,
|
|
||||||
ParentId = parentId,
|
ParentId = parentId,
|
||||||
AutomationId = view.AutomationId ?? platformElement.Id,
|
AutomationId = view.AutomationId ?? platformElement.Id,
|
||||||
Type = view.GetType().Name,
|
Type = view.GetType().Name,
|
||||||
|
@ -120,7 +121,8 @@ namespace Microsoft.Maui.Automation
|
||||||
e.Text = image.Source?.ToString() ?? "";
|
e.Text = image.Source?.ToString() ?? "";
|
||||||
|
|
||||||
|
|
||||||
e.Children.AddRange(view.GetChildren(application, parentId));
|
if (maxDepth <= 0 || (currentDepth + 1 <= maxDepth))
|
||||||
|
e.Children.AddRange(view.GetChildren(application, parentId, currentDepth + 1, maxDepth));
|
||||||
|
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Grpc.Core.Xamarin" Version="2.44.0" />
|
|
||||||
<PackageReference Include="Google.Protobuf" Version="3.21.5" />
|
<PackageReference Include="Google.Protobuf" Version="3.21.5" />
|
||||||
<PackageReference Include="Grpc.Tools" Version="2.48.0">
|
<PackageReference Include="Grpc.Tools" Version="2.48.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using Android.App;
|
using Android.App;
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Android.OS;
|
using Android.OS;
|
||||||
|
using Android.Views;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
@ -44,9 +45,34 @@ namespace Microsoft.Maui.Automation
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<IEnumerable<Element>> GetElements(Platform platform, string elementId = null, int depth = 0)
|
public override Task<IEnumerable<Element>> GetElements(Platform platform)
|
||||||
{
|
{
|
||||||
return Task.FromResult(LifecycleListener.Activities.Select(a => a.GetElement(this)));
|
return Task.FromResult(LifecycleListener.Activities.Select(a => a.GetElement(this, 1, -1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<IEnumerable<Element>> FindElements(Platform platform, Func<Element, bool> matcher)
|
||||||
|
{
|
||||||
|
var windows = LifecycleListener.Activities.Select(a => a.GetElement(this, 1, 1));
|
||||||
|
|
||||||
|
var matches = new List<Element>();
|
||||||
|
Traverse(platform, windows, matches, matcher);
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<Element>>(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Traverse(Platform platform, IEnumerable<Element> elements, IList<Element> matches, Func<Element, bool> matcher)
|
||||||
|
{
|
||||||
|
foreach (var e in elements)
|
||||||
|
{
|
||||||
|
if (matcher(e))
|
||||||
|
matches.Add(e);
|
||||||
|
|
||||||
|
if (e.PlatformElement is View view)
|
||||||
|
{
|
||||||
|
var children = view.GetChildren(this, e.Id, 1, 1);
|
||||||
|
Traverse(platform, children, matches, matcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class AutomationActivityLifecycleContextListener : Java.Lang.Object, Android.App.Application.IActivityLifecycleCallbacks
|
internal class AutomationActivityLifecycleContextListener : Java.Lang.Object, Android.App.Application.IActivityLifecycleCallbacks
|
||||||
|
|
|
@ -13,27 +13,28 @@ namespace Microsoft.Maui.Automation
|
||||||
{
|
{
|
||||||
public static class AndroidExtensions
|
public static class AndroidExtensions
|
||||||
{
|
{
|
||||||
public static IReadOnlyCollection<Element> GetChildren(this Android.Views.View nativeView, IApplication application, string parentId)
|
public static IReadOnlyCollection<Element> GetChildren(this Android.Views.View nativeView, IApplication application, string parentId = "", int currentDepth = -1, int maxDepth = -1)
|
||||||
{
|
{
|
||||||
var c = new List<Element>();
|
var c = new List<Element>();
|
||||||
|
|
||||||
if (nativeView is ViewGroup vg)
|
if (nativeView is ViewGroup vg)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < vg.ChildCount; i++)
|
for (int i = 0; i < vg.ChildCount; i++)
|
||||||
c.Add(vg.GetChildAt(i).GetElement(application, parentId));
|
c.Add(vg.GetChildAt(i).GetElement(application, parentId, currentDepth, maxDepth));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ReadOnlyCollection<Element>(c.ToList());
|
return new ReadOnlyCollection<Element>(c.ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IReadOnlyCollection<Element> GetChildren(this Android.App.Activity activity, IApplication application, string parentId)
|
public static IReadOnlyCollection<Element> GetChildren(this Android.App.Activity activity, IApplication application, string parentId = "", int currentDepth = -1, int maxDepth = -1)
|
||||||
{
|
{
|
||||||
var rootView = activity.Window?.DecorView?.RootView ??
|
var rootView = activity.Window?.DecorView?.RootView ??
|
||||||
activity.FindViewById(Android.Resource.Id.Content)?.RootView ??
|
activity.FindViewById(Android.Resource.Id.Content)?.RootView ??
|
||||||
activity.Window?.DecorView?.FindViewById(Android.Resource.Id.Content);
|
activity.Window?.DecorView?.FindViewById(Android.Resource.Id.Content);
|
||||||
|
|
||||||
if (rootView is not null)
|
if (rootView is not null)
|
||||||
return new ReadOnlyCollection<Element>(new List<Element> { rootView.GetElement(application, parentId) });
|
return new ReadOnlyCollection<Element>(
|
||||||
|
new List<Element> { rootView.GetElement(application, parentId, currentDepth, maxDepth) });
|
||||||
|
|
||||||
return new ReadOnlyCollection<Element>(new List<Element>());
|
return new ReadOnlyCollection<Element>(new List<Element>());
|
||||||
}
|
}
|
||||||
|
@ -43,7 +44,7 @@ namespace Microsoft.Maui.Automation
|
||||||
if (view is Android.Widget.TextView tv)
|
if (view is Android.Widget.TextView tv)
|
||||||
return tv.Text;
|
return tv.Text;
|
||||||
|
|
||||||
return null;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static string EnsureUniqueId(this Android.Views.View view)
|
internal static string EnsureUniqueId(this Android.Views.View view)
|
||||||
|
@ -91,7 +92,7 @@ namespace Microsoft.Maui.Automation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static Element GetElement(this Android.Views.View androidview, IApplication application, string parentId = "")
|
public static Element GetElement(this Android.Views.View androidview, IApplication application, string parentId = "", int currentDepth = -1, int maxDepth = -1)
|
||||||
{
|
{
|
||||||
var e = new Element(application, Platform.Android, androidview.EnsureUniqueId(), androidview, parentId)
|
var e = new Element(application, Platform.Android, androidview.EnsureUniqueId(), androidview, parentId)
|
||||||
{
|
{
|
||||||
|
@ -113,11 +114,12 @@ namespace Microsoft.Maui.Automation
|
||||||
e.Y = loc[1];
|
e.Y = loc[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Children.AddRange(androidview.GetChildren(e.Application, e.Id));
|
if (maxDepth <= 0 || (currentDepth + 1 <= maxDepth))
|
||||||
|
e.Children.AddRange(androidview.GetChildren(e.Application, e.Id, currentDepth + 1, maxDepth));
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Element GetElement(this Activity activity, IApplication application)
|
public static Element GetElement(this Activity activity, IApplication application, int currentDepth = -1, int maxDepth = -1)
|
||||||
{
|
{
|
||||||
var e = new Element(application, Platform.Android, activity.GetWindowId(), activity)
|
var e = new Element(application, Platform.Android, activity.GetWindowId(), activity)
|
||||||
{
|
{
|
||||||
|
@ -137,7 +139,8 @@ namespace Microsoft.Maui.Automation
|
||||||
e.Focused = isCurrent;
|
e.Focused = isCurrent;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Children.AddRange(activity.GetChildren(e.Application, e.Id));
|
if (maxDepth <= 0 || (currentDepth + 1 <= maxDepth))
|
||||||
|
e.Children.AddRange(activity.GetChildren(e.Application, e.Id, currentDepth + 1, maxDepth));
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
using System;
|
using Microsoft.Maui.Controls;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -9,50 +12,54 @@ namespace Microsoft.Maui.Automation
|
||||||
{
|
{
|
||||||
public override Platform DefaultPlatform => Platform.Winappsdk;
|
public override Platform DefaultPlatform => Platform.Winappsdk;
|
||||||
|
|
||||||
async Task<T> RunOnMainThreadAsync<T>(Func<Task<T>> action)
|
|
||||||
{
|
|
||||||
var tcs = new TaskCompletionSource<T>();
|
|
||||||
|
|
||||||
#pragma warning disable VSTHRD101 // Avoid unsupported async delegates
|
public override Task<IEnumerable<Element>> GetElements(Platform platform)
|
||||||
_ = UI.Xaml.Window.Current.Dispatcher.RunAsync(global::Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
|
=> Task.FromResult<IEnumerable<Element>>(new[] { UI.Xaml.Window.Current.GetElement(this, 1, -1) });
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
tcs.TrySetResult(await action());
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
tcs.TrySetException(ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
#pragma warning restore VSTHRD101 // Avoid unsupported async delegates
|
|
||||||
|
|
||||||
return await tcs.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<IEnumerable<Element>> GetElements(Platform platform, string elementId = null, int depth = 0)
|
|
||||||
{
|
|
||||||
var root = await GetRootElements();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(elementId))
|
|
||||||
return root;
|
|
||||||
|
|
||||||
return root.FindDepthFirst(new IdSelector(elementId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<string> GetProperty(Platform platform, string elementId, string propertyName)
|
public override async Task<string> GetProperty(Platform platform, string elementId, string propertyName)
|
||||||
{
|
{
|
||||||
var roots = await GetRootElements();
|
var matches = await FindElements(platform, e => e.Id?.Equals(elementId) ?? false);
|
||||||
|
|
||||||
var element = roots.FindDepthFirst(new IdSelector(elementId))?.FirstOrDefault();
|
var match = matches?.FirstOrDefault();
|
||||||
|
|
||||||
return element.GetType().GetProperty(propertyName)?.GetValue(element)?.ToString() ?? string.Empty;
|
if (match is null)
|
||||||
|
return "";
|
||||||
|
|
||||||
|
return match.GetType().GetProperty(propertyName)?.GetValue(match)?.ToString() ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task<IEnumerable<Element>> GetRootElements()
|
|
||||||
|
public override async Task<IEnumerable<Element>> FindElements(Platform platform, Func<Element, bool> matcher)
|
||||||
{
|
{
|
||||||
var e = await RunOnMainThreadAsync(() => Task.FromResult(UI.Xaml.Window.Current.GetElement(this)));
|
var windows = new[] { UI.Xaml.Window.Current.GetElement(this, 1, 1) };
|
||||||
return new[] { e };
|
|
||||||
|
var matches = new List<Element>();
|
||||||
|
|
||||||
|
await Traverse(platform, windows, matches, matcher);
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task Traverse(Platform platform, IEnumerable<Element> elements, IList<Element> matches, Func<Element, bool> matcher)
|
||||||
|
{
|
||||||
|
foreach (var e in elements)
|
||||||
|
{
|
||||||
|
if (matcher(e))
|
||||||
|
matches.Add(e);
|
||||||
|
|
||||||
|
if (e.PlatformElement is UIElement uiElement)
|
||||||
|
{
|
||||||
|
var children = new List<Element>();
|
||||||
|
|
||||||
|
foreach (var child in (uiElement as Panel)?.Children ?? Enumerable.Empty<UIElement>())
|
||||||
|
{
|
||||||
|
var c = child.GetElement(this, e.Id, 1, 1);
|
||||||
|
children.Add(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Traverse(platform, children, matches, matcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -13,9 +13,9 @@ namespace Microsoft.Maui.Automation;
|
||||||
|
|
||||||
internal static class WindowsExtensions
|
internal static class WindowsExtensions
|
||||||
{
|
{
|
||||||
public static Element GetElement(this UIElement uiElement, IApplication application, string parentId = "")
|
public static Element GetElement(this UIElement uiElement, IApplication application, string parentId = "", int currentDepth = -1, int maxDepth = -1)
|
||||||
{
|
{
|
||||||
var e = new Element(application, Platform.Winappsdk, uiElement.GetHashCode().ToString(), uiElement, parentId)
|
var element = new Element(application, Platform.Winappsdk, uiElement.GetHashCode().ToString(), uiElement, parentId)
|
||||||
{
|
{
|
||||||
AutomationId = uiElement.GetType().Name,
|
AutomationId = uiElement.GetType().Name,
|
||||||
Visible = uiElement.Visibility == UI.Xaml.Visibility.Visible,
|
Visible = uiElement.Visibility == UI.Xaml.Visibility.Visible,
|
||||||
|
@ -27,15 +27,21 @@ internal static class WindowsExtensions
|
||||||
Height = (int)uiElement.ActualSize.Y
|
Height = (int)uiElement.ActualSize.Y
|
||||||
};
|
};
|
||||||
|
|
||||||
var children = (uiElement as Panel)?.Children?.Select(c => c.GetElement(application, e.Id))?.ToList() ?? new List<Element>();
|
if (maxDepth <= 0 || (currentDepth + 1 <= maxDepth))
|
||||||
e.Children.AddRange(children);
|
{
|
||||||
|
foreach (var child in (uiElement as Panel)?.Children ?? Enumerable.Empty<UIElement>())
|
||||||
|
{
|
||||||
|
var c = child.GetElement(application, element.Id, currentDepth + 1, maxDepth);
|
||||||
|
element.Children.Add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return e;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Element GetElement(this Microsoft.UI.Xaml.Window window, IApplication application, string parentId = "")
|
public static Element GetElement(this Microsoft.UI.Xaml.Window window, IApplication application, int currentDepth = -1, int maxDepth = -1)
|
||||||
{
|
{
|
||||||
var e = new Element(application, Platform.Winappsdk, window.GetHashCode().ToString(), window, parentId)
|
var element = new Element(application, Platform.Winappsdk, window.GetHashCode().ToString(), window)
|
||||||
{
|
{
|
||||||
PlatformElement = window,
|
PlatformElement = window,
|
||||||
AutomationId = window.GetType().Name,
|
AutomationId = window.GetType().Name,
|
||||||
|
@ -47,8 +53,12 @@ internal static class WindowsExtensions
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
e.Children.Add(window.Content.GetElement(application, e.Id));
|
if (maxDepth <= 0 || (currentDepth + 1 <= maxDepth))
|
||||||
|
{
|
||||||
|
var c = window.Content.GetElement(application, element.Id, currentDepth + 1, maxDepth);
|
||||||
|
element.Children.Add(c);
|
||||||
|
}
|
||||||
|
|
||||||
return e;
|
return element;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,57 +7,61 @@ using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Microsoft.Maui.Automation
|
namespace Microsoft.Maui.Automation
|
||||||
{
|
{
|
||||||
public class ElementNotFoundException : Exception
|
public class ElementNotFoundException : Exception
|
||||||
{
|
{
|
||||||
public ElementNotFoundException(string elementId)
|
public ElementNotFoundException(string elementId)
|
||||||
: base($"Element with the ID: '{elementId}' was not found.")
|
: base($"Element with the ID: '{elementId}' was not found.")
|
||||||
{
|
{
|
||||||
ElementId = elementId;
|
ElementId = elementId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly string ElementId;
|
public readonly string ElementId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class Application : IApplication
|
public abstract class Application : IApplication
|
||||||
{
|
{
|
||||||
public virtual void Close()
|
public virtual void Close()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
~Application()
|
~Application()
|
||||||
{
|
{
|
||||||
Dispose(false);
|
Dispose(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Dispose()
|
public virtual void Dispose()
|
||||||
{
|
{
|
||||||
Dispose(true);
|
Dispose(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool disposed;
|
bool disposed;
|
||||||
void Dispose(bool disposing)
|
void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (!disposed)
|
if (!disposed)
|
||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
DisposeManagedResources();
|
DisposeManagedResources();
|
||||||
DisposeUnmanagedResources();
|
DisposeUnmanagedResources();
|
||||||
disposed = true;
|
disposed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void DisposeManagedResources()
|
protected virtual void DisposeManagedResources()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void DisposeUnmanagedResources()
|
protected virtual void DisposeUnmanagedResources()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Platform DefaultPlatform { get; }
|
public abstract Platform DefaultPlatform { get; }
|
||||||
|
|
||||||
public abstract Task<string> GetProperty(Platform platform, string elementId, string propertyName);
|
public abstract Task<string> GetProperty(Platform platform, string elementId, string propertyName);
|
||||||
|
|
||||||
public abstract Task<IEnumerable<Element>> GetElements(Platform platform, string? elementId = null, int depth = 0);
|
public abstract Task<IEnumerable<Element>> GetElements(Platform platform);
|
||||||
}
|
|
||||||
|
public abstract Task<IEnumerable<Element>> FindElements(Platform platform, Func<Element, bool> matcher);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.Maui.Automation
|
|
||||||
{
|
|
||||||
public static class ApiExtensions
|
|
||||||
{
|
|
||||||
//public static Task<IEnumerable<Element>> By(this Element element, params IElementSelector[] selectors)
|
|
||||||
// => All(element.Application, element.Platform, selectors);
|
|
||||||
|
|
||||||
//public static async Task<Element?> FirstBy(this Element element, params IElementSelector[] selectors)
|
|
||||||
// => (await By(element.Application, element.Platform, selectors)).FirstOrDefault();
|
|
||||||
|
|
||||||
//public static Task<IEnumerable<Element>> By(this IApplication app, Platform platform, params IElementSelector[] selectors)
|
|
||||||
// => All(app, platform, selectors);
|
|
||||||
|
|
||||||
//public static async Task<Element?> FirstBy(this IApplication app, Platform platform, params IElementSelector[] selectors)
|
|
||||||
// => (await By(app, platform, selectors))?.FirstOrDefault();
|
|
||||||
|
|
||||||
//public static Task<IEnumerable<Element>> ByAutomationId(this IApplication app, Platform platform, string automationId, StringComparison comparison = StringComparison.Ordinal)
|
|
||||||
// => By(app, platform, new AutomationIdSelector(automationId, comparison));
|
|
||||||
|
|
||||||
//public static Task<Element?> ById(this IApplication app, Platform platform, string id, StringComparison comparison = StringComparison.Ordinal)
|
|
||||||
// => FirstBy(app, platform, new IdSelector(id, comparison));
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//public static Task<IEnumerable<Element>> All(this IApplication app, Platform platform, params IElementSelector[] selectors)
|
|
||||||
// => app.GetElements(platform, selector: new CompoundSelector(any: false, selectors));
|
|
||||||
|
|
||||||
//public static Task<IEnumerable<Element>> Any(this IApplication app, Platform platform, params IElementSelector[] selectors)
|
|
||||||
// => app.GetElements(platform, selector: new CompoundSelector(any: true, selectors));
|
|
||||||
|
|
||||||
//public static Task<IEnumerable<Element>> All(this Element element, Platform platform, params IElementSelector[] selectors)
|
|
||||||
// => element.Application.GetElements(platform, element.Id, new CompoundSelector(any: false, selectors));
|
|
||||||
|
|
||||||
//public static Task<IEnumerable<Element>> Any(this Element element, Platform platform, params IElementSelector[] selectors)
|
|
||||||
// => element.Application.GetElements(platform, element.Id, new CompoundSelector(any: true, selectors));
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
using Grpc.Core;
|
||||||
|
using Grpc.Net.Client;
|
||||||
|
using Microsoft.Maui.Automation.RemoteGrpc;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.Maui.Automation
|
||||||
|
{
|
||||||
|
public class GrpcRemoteAppAgent : IDisposable
|
||||||
|
{
|
||||||
|
readonly RemoteApp.RemoteAppClient client;
|
||||||
|
readonly IApplication Application;
|
||||||
|
|
||||||
|
readonly AsyncDuplexStreamingCall<ElementsResponse, ElementsRequest> elementsCall;
|
||||||
|
readonly AsyncDuplexStreamingCall<ElementsResponse, FindElementsRequest> findElementsCall;
|
||||||
|
|
||||||
|
readonly Task elementsCallTask;
|
||||||
|
readonly Task findElementsCallTask;
|
||||||
|
private bool disposedValue;
|
||||||
|
|
||||||
|
public GrpcRemoteAppAgent(IApplication application, string address)
|
||||||
|
{
|
||||||
|
Application = application;
|
||||||
|
|
||||||
|
var grpc = GrpcChannel.ForAddress(address);
|
||||||
|
client = new RemoteApp.RemoteAppClient(grpc);
|
||||||
|
|
||||||
|
elementsCall = client.GetElementsRoute();
|
||||||
|
findElementsCall = client.FindElementsRoute();
|
||||||
|
|
||||||
|
elementsCallTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
while (await elementsCall.ResponseStream.MoveNext())
|
||||||
|
{
|
||||||
|
var response = await HandleElementsRequest(elementsCall.ResponseStream.Current);
|
||||||
|
await elementsCall.RequestStream.WriteAsync(response);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
findElementsCallTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
while (await findElementsCall.ResponseStream.MoveNext())
|
||||||
|
{
|
||||||
|
var response = await HandleFindElementsRequest(findElementsCall.ResponseStream.Current);
|
||||||
|
await findElementsCall.RequestStream.WriteAsync(response);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<ElementsResponse> HandleElementsRequest(ElementsRequest request)
|
||||||
|
{
|
||||||
|
// Get the elements from the running app host
|
||||||
|
var elements = await Application.GetElements(request.Platform);
|
||||||
|
|
||||||
|
var response = new ElementsResponse();
|
||||||
|
response.RequestId = request.RequestId;
|
||||||
|
response.Elements.AddRange(elements);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<ElementsResponse> HandleFindElementsRequest(FindElementsRequest request)
|
||||||
|
{
|
||||||
|
// Get the elements from the running app host
|
||||||
|
var elements = await Application.FindElements(request.Platform, e =>
|
||||||
|
{
|
||||||
|
var value =
|
||||||
|
request.PropertyName.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"id" => e.Id,
|
||||||
|
"automationid" => e.AutomationId,
|
||||||
|
"text" => e.Text,
|
||||||
|
"type" => e.Type,
|
||||||
|
"fulltype" => e.FullType,
|
||||||
|
_ => string.Empty
|
||||||
|
} ?? string.Empty;
|
||||||
|
|
||||||
|
return request.IsExpression
|
||||||
|
? Regex.IsMatch(value, request.Pattern)
|
||||||
|
: request.Pattern.Equals(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = new ElementsResponse();
|
||||||
|
response.RequestId = request.RequestId;
|
||||||
|
response.Elements.AddRange(elements);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!disposedValue)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
elementsCall.Dispose();
|
||||||
|
findElementsCall.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
disposedValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(disposing: true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,64 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Microsoft.Maui.Automation
|
|
||||||
{
|
|
||||||
|
|
||||||
internal static class HelperExtensions
|
|
||||||
{
|
|
||||||
internal static void PushAllReverse<T>(this Stack<T> st, IEnumerable<T> elems)
|
|
||||||
{
|
|
||||||
foreach (var elem in elems.Reverse())
|
|
||||||
st.Push(elem);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static IEnumerable<Element> FindDepthFirst(this IEnumerable<Element> elements, IElementSelector? selector)
|
|
||||||
{
|
|
||||||
var st = new Stack<Element>();
|
|
||||||
st.PushAllReverse(elements);
|
|
||||||
|
|
||||||
while (st.Count > 0)
|
|
||||||
{
|
|
||||||
var v = st.Pop();
|
|
||||||
if (selector == null || selector.Matches(v))
|
|
||||||
{
|
|
||||||
yield return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
st.PushAllReverse(v.Children);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static IEnumerable<Element> FindBreadthFirst(this IEnumerable<Element> elements, IElementSelector? selector)
|
|
||||||
{
|
|
||||||
var q = new Queue<Element>();
|
|
||||||
|
|
||||||
foreach (var e in elements)
|
|
||||||
q.Enqueue(e);
|
|
||||||
|
|
||||||
while (q.Count > 0)
|
|
||||||
{
|
|
||||||
var v = q.Dequeue();
|
|
||||||
if (selector == null || selector.Matches(v))
|
|
||||||
{
|
|
||||||
yield return v;
|
|
||||||
}
|
|
||||||
foreach (var c in v.Children)
|
|
||||||
q.Enqueue(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IReadOnlyCollection<T> ToReadOnlyCollection<T>(this IEnumerable<T> elems)
|
|
||||||
{
|
|
||||||
return new ReadOnlyCollection<T>(elems.ToList());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IReadOnlyCollection<Element> AsReadOnlyCollection(this Element element)
|
|
||||||
{
|
|
||||||
var list = new List<Element> { element };
|
|
||||||
return list.AsReadOnly();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +1,16 @@
|
||||||
namespace Microsoft.Maui.Automation
|
using Grpc.Core;
|
||||||
|
using Microsoft.Maui.Automation.RemoteGrpc;
|
||||||
|
|
||||||
|
namespace Microsoft.Maui.Automation
|
||||||
{
|
{
|
||||||
public interface IApplication
|
public interface IApplication
|
||||||
{
|
{
|
||||||
public Platform DefaultPlatform { get; }
|
public Platform DefaultPlatform { get; }
|
||||||
|
|
||||||
public Task<IEnumerable<Element>> GetElements(Platform platform, string? elementId = null, int childDepth = 0);
|
public Task<IEnumerable<Element>> GetElements(Platform platform);
|
||||||
|
|
||||||
|
public Task<IEnumerable<Element>> FindElements(Platform platform, Func<Element, bool> matcher);
|
||||||
|
|
||||||
public Task<string> GetProperty(Platform platform, string elementId, string propertyName);
|
public Task<string> GetProperty(Platform platform, string elementId, string propertyName);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,36 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
//using Google.Protobuf.Collections;
|
|
||||||
|
|
||||||
//namespace Microsoft.Maui.Automation;
|
|
||||||
|
|
||||||
//public interface IElement
|
|
||||||
//{
|
|
||||||
// public IApplication Application { get; }
|
|
||||||
|
|
||||||
// public Platform Platform { get; }
|
|
||||||
|
|
||||||
// public string? ParentId { get; }
|
|
||||||
|
|
||||||
// public bool Visible { get; }
|
|
||||||
// public bool Enabled { get; }
|
|
||||||
// public bool Focused { get; }
|
|
||||||
|
|
||||||
// public object? PlatformElement { get; }
|
|
||||||
|
|
||||||
// public RepeatedField<Element> Children { get; }
|
|
||||||
|
|
||||||
// public string Id { get; }
|
|
||||||
|
|
||||||
// public string AutomationId { get; }
|
|
||||||
|
|
||||||
// public string Type { get; }
|
|
||||||
// public string FullType { get; }
|
|
||||||
|
|
||||||
// public string? Text { get; }
|
|
||||||
|
|
||||||
// public int Width { get; }
|
|
||||||
// public int Height { get; }
|
|
||||||
// public int X { get; }
|
|
||||||
// public int Y { get; }
|
|
||||||
//}
|
|
|
@ -9,6 +9,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Google.Protobuf" Version="3.21.5" />
|
<PackageReference Include="Google.Protobuf" Version="3.21.5" />
|
||||||
<PackageReference Include="Grpc.Core" Version="2.46.3" />
|
<PackageReference Include="Grpc.Core" Version="2.46.3" />
|
||||||
|
<PackageReference Include="Grpc.Net.Client" Version="2.48.0" />
|
||||||
<PackageReference Include="Grpc.Tools" Version="2.48.0">
|
<PackageReference Include="Grpc.Tools" Version="2.48.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|
|
@ -29,11 +29,14 @@ namespace Microsoft.Maui.Automation
|
||||||
return PlatformApps[platform];
|
return PlatformApps[platform];
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<IEnumerable<Element>> GetElements(Platform platform, string? elementId = null, int depth = 0)
|
public override Task<IEnumerable<Element>> GetElements(Platform platform)
|
||||||
=> GetApp(platform).GetElements(platform, elementId, depth);
|
=> GetApp(platform).GetElements(platform);
|
||||||
|
|
||||||
public override Task<string?> GetProperty(Platform platform, string elementId, string propertyName)
|
public override Task<string> GetProperty(Platform platform, string elementId, string propertyName)
|
||||||
=> GetApp(platform).GetProperty(platform, elementId, propertyName);
|
=> GetApp(platform).GetProperty(platform, elementId, propertyName);
|
||||||
|
|
||||||
|
public override Task<IEnumerable<Element>> FindElements(Platform platform, Func<Element, bool> matcher)
|
||||||
|
=> GetApp(platform).FindElements(platform, matcher);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
|
|
||||||
namespace Microsoft.Maui.Automation
|
|
||||||
{
|
|
||||||
|
|
||||||
public class AutomationIdSelector : IElementSelector
|
|
||||||
{
|
|
||||||
public AutomationIdSelector(string automationId, StringComparison comparison = StringComparison.Ordinal)
|
|
||||||
{
|
|
||||||
AutomationId = automationId;
|
|
||||||
Comparison = comparison;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string AutomationId { get; protected set; }
|
|
||||||
|
|
||||||
public StringComparison Comparison { get; protected set; }
|
|
||||||
|
|
||||||
public bool Matches(Element view)
|
|
||||||
=> view?.AutomationId?.Equals(AutomationId, Comparison) ?? false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
namespace Microsoft.Maui.Automation
|
|
||||||
{
|
|
||||||
public class CompoundSelector : IElementSelector
|
|
||||||
{
|
|
||||||
public CompoundSelector(bool any, params IElementSelector[] elementSelectors)
|
|
||||||
{
|
|
||||||
Selectors = elementSelectors;
|
|
||||||
Any = any;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IElementSelector[] Selectors { get; protected set; }
|
|
||||||
|
|
||||||
public bool Any { get; protected set; }
|
|
||||||
|
|
||||||
public bool Matches(Element element)
|
|
||||||
{
|
|
||||||
foreach (var s in Selectors)
|
|
||||||
{
|
|
||||||
var isMatch = s.Matches(element);
|
|
||||||
|
|
||||||
// If looking for any to match, and we found a match, return true
|
|
||||||
if (Any && isMatch)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
// If we want ALL to match and we found a non-match, return false
|
|
||||||
if (!Any && !isMatch)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we get here, we went through all the selectors and all did or all did not match
|
|
||||||
// Otherwise we would have short circuited out the loop earlier
|
|
||||||
// If we were looking for ANY, we had no matches, return false
|
|
||||||
// If we are looking for ALL, we had all matches, return true
|
|
||||||
return !Any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
namespace Microsoft.Maui.Automation
|
|
||||||
{
|
|
||||||
public class DefaultViewSelector : IElementSelector
|
|
||||||
{
|
|
||||||
public DefaultViewSelector()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Matches(Element view)
|
|
||||||
=> true;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
|
|
||||||
namespace Microsoft.Maui.Automation
|
|
||||||
{
|
|
||||||
public interface IElementSelector
|
|
||||||
{
|
|
||||||
public bool Matches(Element element);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
namespace Microsoft.Maui.Automation
|
|
||||||
{
|
|
||||||
public class IdSelector : IElementSelector
|
|
||||||
{
|
|
||||||
public IdSelector(string id, StringComparison comparison = StringComparison.Ordinal)
|
|
||||||
{
|
|
||||||
Id = id;
|
|
||||||
Comparison = comparison;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Id { get; protected set; }
|
|
||||||
|
|
||||||
public StringComparison Comparison { get; protected set; }
|
|
||||||
|
|
||||||
public bool Matches(Element view)
|
|
||||||
=> view?.AutomationId?.Equals(Id, Comparison) ?? false;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace Microsoft.Maui.Automation
|
|
||||||
{
|
|
||||||
public class RegularExpressionSelector : IElementSelector
|
|
||||||
{
|
|
||||||
public RegularExpressionSelector(string pattern, RegexOptions options = RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace)
|
|
||||||
{
|
|
||||||
Pattern = pattern;
|
|
||||||
Options = options;
|
|
||||||
Rx = new Regex(Pattern, Options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Pattern { get; protected set; }
|
|
||||||
|
|
||||||
public RegexOptions Options { get; protected set; }
|
|
||||||
|
|
||||||
public Regex Rx { get; protected set; }
|
|
||||||
|
|
||||||
public bool Matches(Element view)
|
|
||||||
=> view.Text != null && Rx.IsMatch(view.Text);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
|
|
||||||
namespace Microsoft.Maui.Automation
|
|
||||||
{
|
|
||||||
public class TextSelector : IElementSelector
|
|
||||||
{
|
|
||||||
public TextSelector(string text, TextMatchRule rule = TextMatchRule.Contains, StringComparison comparison = StringComparison.InvariantCultureIgnoreCase)
|
|
||||||
{
|
|
||||||
Text = text;
|
|
||||||
Rule = rule;
|
|
||||||
Comparison = comparison;
|
|
||||||
}
|
|
||||||
|
|
||||||
public StringComparison Comparison { get; protected set; }
|
|
||||||
|
|
||||||
public TextMatchRule Rule { get; protected set; }
|
|
||||||
|
|
||||||
public string Text { get; protected set; }
|
|
||||||
|
|
||||||
public bool Matches(Element view)
|
|
||||||
=> Rule switch
|
|
||||||
{
|
|
||||||
TextMatchRule.Contains => view.Text?.Contains(Text, Comparison) ?? false,
|
|
||||||
TextMatchRule.StartsWith => view.Text?.StartsWith(Text, Comparison) ?? false,
|
|
||||||
TextMatchRule.EndsWith => view.Text?.EndsWith(Text, Comparison) ?? false,
|
|
||||||
TextMatchRule.Exact => view.Text?.Equals(Text, Comparison) ?? false,
|
|
||||||
_ => view.Text?.Contains(Text, Comparison) ?? false
|
|
||||||
};
|
|
||||||
|
|
||||||
public enum TextMatchRule
|
|
||||||
{
|
|
||||||
Contains,
|
|
||||||
StartsWith,
|
|
||||||
EndsWith,
|
|
||||||
Exact
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
namespace Microsoft.Maui.Automation
|
|
||||||
{
|
|
||||||
public class TypeSelector : IElementSelector
|
|
||||||
{
|
|
||||||
internal TypeSelector()
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public TypeSelector(string typeName, bool fullName = false)
|
|
||||||
{
|
|
||||||
TypeName = typeName;
|
|
||||||
FullName = fullName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TypeSelector(Type type, bool fullName = false)
|
|
||||||
{
|
|
||||||
TypeName = type.Name;
|
|
||||||
FullName = fullName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string TypeName { get; protected set; }
|
|
||||||
|
|
||||||
public bool FullName { get; protected set; }
|
|
||||||
|
|
||||||
public bool Matches(Element element)
|
|
||||||
=> element.Type.Equals(TypeName);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +1,18 @@
|
||||||
namespace Microsoft.Maui.Automation
|
namespace Microsoft.Maui.Automation
|
||||||
{
|
{
|
||||||
public static class ViewExtensions
|
public static class ViewExtensions
|
||||||
{
|
{
|
||||||
public static bool IsTopLevel(this Element element)
|
public static bool IsTopLevel(this Element element)
|
||||||
=> element?.ParentId == element?.Id;
|
=> element?.ParentId == element?.Id;
|
||||||
|
|
||||||
|
|
||||||
public static string ToString(this Element element, int depth, int indentSpaces = 2)
|
public static string ToString(this Element element, int depth, int indentSpaces = 2)
|
||||||
{
|
{
|
||||||
var v = element;
|
var v = element;
|
||||||
var t = element.IsTopLevel() ? "window" : "view";
|
var t = element.IsTopLevel() ? "window" : "view";
|
||||||
var s = "\r\n" + new string(' ', (depth * indentSpaces) + indentSpaces);
|
var s = "\r\n" + new string(' ', (depth * indentSpaces) + indentSpaces);
|
||||||
return $"[{t}:{v.Type} id='{v.Id}',{s}parentId='{v.ParentId}',{s}automationId='{v.AutomationId}',{s}visible='{v.Visible}',{s}enabled='{v.Enabled}',{s}focused='{v.Focused}',{s}frame='{v.X}x,{v.Y}y,{v.Width}w,{v.Height}h',{s}children={v.Children.Count}]";
|
return $"[{t}:{v.Type} id='{v.Id}',{s}parentId='{v.ParentId}',{s}automationId='{v.AutomationId}',{s}visible='{v.Visible}',{s}enabled='{v.Enabled}',{s}focused='{v.Focused}',{s}frame='{v.X}x,{v.Y}y,{v.Width}w,{v.Height}h',{s}children={v.Children.Count}]";
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,186 @@
|
||||||
|
using AndroidSdk;
|
||||||
|
using Grpc.Net.Client;
|
||||||
|
using Microsoft.Maui.Automation.Remote;
|
||||||
|
using Spectre.Console.Cli;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.NetworkInformation;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.Maui.Automation.Driver
|
||||||
|
{
|
||||||
|
public class AndroidDriver : IDriver
|
||||||
|
{
|
||||||
|
public AndroidDriver(string adbDeviceSerial)
|
||||||
|
{
|
||||||
|
int port = 10882;
|
||||||
|
var address = IPAddress.Any.ToString();
|
||||||
|
|
||||||
|
androidSdkManager = new AndroidSdk.AndroidSdkManager();
|
||||||
|
Adb = androidSdkManager.Adb;
|
||||||
|
Pm = androidSdkManager.PackageManager;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(adbDeviceSerial))
|
||||||
|
{
|
||||||
|
var anySerial = Adb.GetDevices()?.FirstOrDefault()?.Serial;
|
||||||
|
if (!string.IsNullOrEmpty(anySerial))
|
||||||
|
adbDeviceSerial = anySerial;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArgumentNullException.ThrowIfNull(adbDeviceSerial);
|
||||||
|
Device = adbDeviceSerial;
|
||||||
|
Pm.AdbSerial = adbDeviceSerial;
|
||||||
|
|
||||||
|
Name = $"Android ({Adb.GetDeviceName(Device)})";
|
||||||
|
|
||||||
|
var forwardResult = Adb.RunCommand("reverse", $"tcp:{port}", $"tcp:{port}")?.GetAllOutput();
|
||||||
|
Console.WriteLine(forwardResult);
|
||||||
|
|
||||||
|
grpc = new GrpcRemoteAppClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly GrpcRemoteAppClient grpc;
|
||||||
|
|
||||||
|
protected readonly AndroidSdk.Adb Adb;
|
||||||
|
protected readonly AndroidSdk.PackageManager Pm;
|
||||||
|
|
||||||
|
readonly AndroidSdk.AndroidSdkManager androidSdkManager;
|
||||||
|
protected readonly string Device;
|
||||||
|
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
public Task Back()
|
||||||
|
{
|
||||||
|
Adb.Shell($"input keyevent 4", Device);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ClearAppState(string appId)
|
||||||
|
{
|
||||||
|
if (!IsAppInstalled(appId))
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
Adb.Shell($"pm clear {appId}", Device);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task InstallApp(string file, string appId)
|
||||||
|
{
|
||||||
|
Adb.Install(new FileInfo(file), Device);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RemoveApp(string appId)
|
||||||
|
{
|
||||||
|
Adb.Uninstall(appId, false, Device);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IDeviceInfo> GetDeviceInfo()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task InputText(string text)
|
||||||
|
{
|
||||||
|
Adb.Shell($"input text {text}", Device);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task KeyPress(string keyCode)
|
||||||
|
{
|
||||||
|
// Enter = 66
|
||||||
|
// Backspace = 67
|
||||||
|
// Back = 4
|
||||||
|
// Volume Up = 24
|
||||||
|
// VolumeDown = 25
|
||||||
|
// Home = 3
|
||||||
|
// Lock 276
|
||||||
|
|
||||||
|
var code = keyCode?.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"enter" => 66,
|
||||||
|
"backspace" => 67,
|
||||||
|
"back" => 4,
|
||||||
|
"volumeup" => 24,
|
||||||
|
"volumedown" => 25,
|
||||||
|
"home" => 3,
|
||||||
|
"lock" => 276,
|
||||||
|
_ => 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// adb shell input keyevent $code
|
||||||
|
if (code <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(keyCode), "Unknown KeyCode");
|
||||||
|
|
||||||
|
Adb.Shell($"input keyevent {code}", Device);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task LaunchApp(string appId)
|
||||||
|
{
|
||||||
|
// First force stop existing
|
||||||
|
Adb.Shell($"am force-stop {appId}", Device);
|
||||||
|
|
||||||
|
// Launch app's activity
|
||||||
|
Adb.Shell($"monkey -p {appId} -c android.intent.category.LAUNCHER 1", Device);
|
||||||
|
//Adb.Shell($"monkey --pct-syskeys 0 -p {appId} 1", Device);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task LongPress(int x, int y)
|
||||||
|
{
|
||||||
|
// Use a swipe that doesn't move as long press
|
||||||
|
Adb.Shell($"input swipe {x} {y} {x} {y} 3000", Device);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task OpenUri(string uri)
|
||||||
|
{
|
||||||
|
Adb.Shell($"am start -d {uri}", Device);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Scroll()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopApp(string appId)
|
||||||
|
{
|
||||||
|
// Force the app to stop
|
||||||
|
// am force-stop $appId"
|
||||||
|
Adb.Shell($"am force-stop {appId}", Device);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Swipe((int x, int y) start, (int x, int y) end)
|
||||||
|
{
|
||||||
|
Adb.Shell($"input swipe {start.x} {start.y} {end.x} {end.y} 2000", Device);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Tap(int x, int y)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsAppInstalled(string appId)
|
||||||
|
=> androidSdkManager.PackageManager.ListPackages()
|
||||||
|
.Any(p => p.PackageName?.Equals(appId, StringComparison.OrdinalIgnoreCase) ?? false);
|
||||||
|
|
||||||
|
public Task<string> GetProperty(Platform platform, string elementId, string propertyName)
|
||||||
|
{
|
||||||
|
return Task.FromResult("");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IEnumerable<Element>> GetElements(Platform platform)
|
||||||
|
=> grpc.GetElements(platform);
|
||||||
|
|
||||||
|
public Task<IEnumerable<Element>> FindElements(Platform platform, string propertyName, string pattern, bool isExpression = false, string ancestorId = "")
|
||||||
|
=> grpc.FindElements(platform, propertyName, pattern, isExpression, ancestorId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
using Grpc.Core;
|
||||||
|
using Microsoft.Maui.Automation.RemoteGrpc;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.Maui.Automation.Remote
|
||||||
|
{
|
||||||
|
public class GrpcRemoteAppClient : RemoteGrpc.RemoteApp.RemoteAppBase
|
||||||
|
{
|
||||||
|
public GrpcRemoteAppClient()
|
||||||
|
{
|
||||||
|
server = new Server
|
||||||
|
{
|
||||||
|
Services = { RemoteApp.BindService(this) },
|
||||||
|
Ports = { new ServerPort("127.0.0.1", 10882, ServerCredentials.Insecure) }
|
||||||
|
};
|
||||||
|
server.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly Server server;
|
||||||
|
|
||||||
|
Dictionary<string, TaskCompletionSource<IEnumerable<Element>>> pendingElementResponses = new();
|
||||||
|
|
||||||
|
TaskCompletionSource<IAsyncStreamWriter<ElementsRequest>> elementsRequestStream = new ();
|
||||||
|
TaskCompletionSource<IAsyncStreamWriter<FindElementsRequest>> findElementsRequestStream = new();
|
||||||
|
|
||||||
|
public override async Task GetElementsRoute(IAsyncStreamReader<ElementsResponse> requestStream, IServerStreamWriter<ElementsRequest> responseStream, ServerCallContext context)
|
||||||
|
{
|
||||||
|
elementsRequestStream.TrySetResult(responseStream);
|
||||||
|
|
||||||
|
while (await requestStream.MoveNext())
|
||||||
|
{
|
||||||
|
var requestId = requestStream.Current.RequestId;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(requestId) && pendingElementResponses.ContainsKey(requestId))
|
||||||
|
{
|
||||||
|
var tcs = pendingElementResponses[requestId];
|
||||||
|
pendingElementResponses.Remove(requestId);
|
||||||
|
tcs.TrySetResult(requestStream.Current.Elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task FindElementsRoute(IAsyncStreamReader<ElementsResponse> requestStream, IServerStreamWriter<FindElementsRequest> responseStream, ServerCallContext context)
|
||||||
|
{
|
||||||
|
findElementsRequestStream.TrySetResult(responseStream);
|
||||||
|
|
||||||
|
while (await requestStream.MoveNext())
|
||||||
|
{
|
||||||
|
var requestId = requestStream.Current.RequestId;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(requestId) && pendingElementResponses.ContainsKey(requestId))
|
||||||
|
{
|
||||||
|
var tcs = pendingElementResponses[requestId];
|
||||||
|
pendingElementResponses.Remove(requestId);
|
||||||
|
tcs.TrySetResult(requestStream.Current.Elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Element>> GetElements(Platform platform)
|
||||||
|
{
|
||||||
|
var stream = await elementsRequestStream.Task;
|
||||||
|
var requestId = Guid.NewGuid().ToString();
|
||||||
|
var request = new ElementsRequest { Platform = platform, RequestId = requestId };
|
||||||
|
|
||||||
|
var tcs = new TaskCompletionSource<IEnumerable<Element>>();
|
||||||
|
pendingElementResponses.Add(requestId, tcs);
|
||||||
|
|
||||||
|
await stream.WriteAsync(request);
|
||||||
|
|
||||||
|
return await tcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Element>> FindElements(Platform platform, string propertyName, string pattern, bool isExpression = false, string ancestorId = "")
|
||||||
|
{
|
||||||
|
var stream = await findElementsRequestStream.Task;
|
||||||
|
var requestId = Guid.NewGuid().ToString();
|
||||||
|
var request = new FindElementsRequest
|
||||||
|
{
|
||||||
|
Platform = platform,
|
||||||
|
RequestId = requestId,
|
||||||
|
AncestorId = ancestorId,
|
||||||
|
IsExpression = isExpression,
|
||||||
|
Pattern= pattern,
|
||||||
|
PropertyName = propertyName
|
||||||
|
};
|
||||||
|
|
||||||
|
var tcs = new TaskCompletionSource<IEnumerable<Element>>();
|
||||||
|
pendingElementResponses.Add(requestId, tcs);
|
||||||
|
|
||||||
|
await stream.WriteAsync(request);
|
||||||
|
|
||||||
|
return await tcs.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Shutdown()
|
||||||
|
{
|
||||||
|
await server.ShutdownAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace Microsoft.Maui.Automation.Driver
|
||||||
|
{
|
||||||
|
|
||||||
|
public interface IDeviceInfo
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
namespace Microsoft.Maui.Automation.Driver
|
||||||
|
{
|
||||||
|
public interface IDriver
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
|
||||||
|
Task<IDeviceInfo> GetDeviceInfo();
|
||||||
|
|
||||||
|
Task InstallApp(string file, string appId);
|
||||||
|
Task RemoveApp(string appId);
|
||||||
|
|
||||||
|
Task LaunchApp(string appId);
|
||||||
|
Task StopApp(string appId);
|
||||||
|
|
||||||
|
Task ClearAppState(string appId);
|
||||||
|
|
||||||
|
Task Tap(int x, int y);
|
||||||
|
|
||||||
|
Task LongPress(int x, int y);
|
||||||
|
|
||||||
|
Task KeyPress(string keyCode);
|
||||||
|
|
||||||
|
Task Scroll();
|
||||||
|
|
||||||
|
Task Swipe((int x, int y) start, (int x, int y) end);
|
||||||
|
|
||||||
|
Task Back();
|
||||||
|
|
||||||
|
Task InputText(string text);
|
||||||
|
|
||||||
|
Task OpenUri(string uri);
|
||||||
|
|
||||||
|
Task<string> GetProperty(Platform platform, string elementId, string propertyName);
|
||||||
|
|
||||||
|
Task<IEnumerable<Element>> GetElements(Platform platform);
|
||||||
|
|
||||||
|
Task<IEnumerable<Element>> FindElements(Platform platform, string propertyName, string pattern, bool isExpression = false, string ancestorId = "");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AndroidSdk" Version="0.5.2" />
|
||||||
|
<PackageReference Include="Grpc.Core" Version="2.46.3" />
|
||||||
|
<PackageReference Include="Grpc.Net.Client" Version="2.48.0" />
|
||||||
|
<PackageReference Include="Spectre.Console" Version="0.44.0" />
|
||||||
|
<PackageReference Include="Google.Protobuf" Version="3.21.5" />
|
||||||
|
<PackageReference Include="Grpc.Tools" Version="2.48.0">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Microsoft.Maui.Automation.Core\Microsoft.Maui.Automation.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -20,6 +20,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Microsoft.Maui.Automation.Core\Microsoft.Maui.Automation.Core.csproj" />
|
<ProjectReference Include="..\Microsoft.Maui.Automation.Core\Microsoft.Maui.Automation.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\Microsoft.Maui.Automation.Driver\Microsoft.Maui.Automation.Driver.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using Grpc.Net.Client;
|
using Grpc.Net.Client;
|
||||||
using Microsoft.Maui.Automation;
|
using Microsoft.Maui.Automation;
|
||||||
|
using Microsoft.Maui.Automation.Remote;
|
||||||
using Spectre.Console;
|
using Spectre.Console;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
||||||
|
@ -8,12 +9,7 @@ var address = "http://localhost:10882";
|
||||||
Console.WriteLine($"REPL> Connecting to {address}...");
|
Console.WriteLine($"REPL> Connecting to {address}...");
|
||||||
var platform = Platform.Maui;
|
var platform = Platform.Maui;
|
||||||
|
|
||||||
|
var grpc = new GrpcRemoteAppClient();
|
||||||
|
|
||||||
var grpc = GrpcChannel.ForAddress(address);
|
|
||||||
|
|
||||||
var client = new Microsoft.Maui.Automation.RemoteGrpc.RemoteApp.RemoteAppClient(grpc);
|
|
||||||
|
|
||||||
|
|
||||||
Console.WriteLine("Connected.");
|
Console.WriteLine("Connected.");
|
||||||
|
|
||||||
|
@ -26,9 +22,9 @@ while (true)
|
||||||
{
|
{
|
||||||
if (input.StartsWith("tree"))
|
if (input.StartsWith("tree"))
|
||||||
{
|
{
|
||||||
var children = await client.GetElementsAsync(new Microsoft.Maui.Automation.RemoteGrpc.ElementsRequest());
|
var children = await grpc.GetElements(platform);
|
||||||
|
|
||||||
foreach (var w in children.Elements)
|
foreach (var w in children)
|
||||||
{
|
{
|
||||||
var tree = new Tree(w.ToTable(ConfigureTable));
|
var tree = new Tree(w.ToTable(ConfigureTable));
|
||||||
|
|
||||||
|
@ -44,9 +40,9 @@ while (true)
|
||||||
}
|
}
|
||||||
else if (input.StartsWith("windows"))
|
else if (input.StartsWith("windows"))
|
||||||
{
|
{
|
||||||
var children = await client.GetElementsAsync(new Microsoft.Maui.Automation.RemoteGrpc.ElementsRequest());
|
var children = await grpc.GetElements(platform);
|
||||||
|
|
||||||
foreach (var w in children.Elements)
|
foreach (var w in children)
|
||||||
{
|
{
|
||||||
var tree = new Tree(w.ToTable(ConfigureTable));
|
var tree = new Tree(w.ToTable(ConfigureTable));
|
||||||
|
|
||||||
|
@ -55,11 +51,10 @@ while (true)
|
||||||
}
|
}
|
||||||
else if (input.StartsWith("test"))
|
else if (input.StartsWith("test"))
|
||||||
{
|
{
|
||||||
var children = await client.GetElementsAsync(new Microsoft.Maui.Automation.RemoteGrpc.ElementsRequest());
|
var elements = await grpc.FindElements(platform, "AutomationId", "buttonOne");
|
||||||
|
|
||||||
foreach (var w in children.Elements)
|
foreach (var w in elements)
|
||||||
{
|
{
|
||||||
|
|
||||||
var tree = new Tree(w.ToTable(ConfigureTable));
|
var tree = new Tree(w.ToTable(ConfigureTable));
|
||||||
|
|
||||||
AnsiConsole.Write(tree);
|
AnsiConsole.Write(tree);
|
||||||
|
@ -77,6 +72,8 @@ while (true)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await grpc.Shutdown();
|
||||||
|
|
||||||
|
|
||||||
void PrintTree(IHasTreeNodes node, Element element, int depth)
|
void PrintTree(IHasTreeNodes node, Element element, int depth)
|
||||||
{
|
{
|
||||||
|
|
|
@ -6,8 +6,22 @@
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
|
||||||
|
<PackageReference Include="xunit" Version="2.4.2" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Microsoft.Maui.Automation.Core\Microsoft.Maui.Automation.Core.csproj" />
|
<ProjectReference Include="..\Microsoft.Maui.Automation.Core\Microsoft.Maui.Automation.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\Microsoft.Maui.Automation.Driver\Microsoft.Maui.Automation.Driver.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Maui.Automation.Test
|
||||||
|
{
|
||||||
|
public class Tests
|
||||||
|
{
|
||||||
|
public Tests()
|
||||||
|
{
|
||||||
|
driver = new Driver.AndroidDriver(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly Microsoft.Maui.Automation.Driver.AndroidDriver driver;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RunApp()
|
||||||
|
{
|
||||||
|
var appId = "com.companyname.samplemauiapp";
|
||||||
|
|
||||||
|
//await driver.InstallApp(@"C:\code\Maui.UITesting\samples\SampleMauiApp\bin\Debug\net6.0-android\com.companyname.samplemauiapp-Signed.apk", appId);
|
||||||
|
await driver.LaunchApp(appId);
|
||||||
|
|
||||||
|
var elements = await driver.FindElements(Platform.Maui, "AutomationId", "buttonOne");
|
||||||
|
|
||||||
|
var e = elements.FirstOrDefault();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Proto", "Proto", "{99757F39
|
||||||
proto\types.proto = proto\types.proto
|
proto\types.proto = proto\types.proto
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Maui.Automation.Driver", "Microsoft.Maui.Automation.Driver\Microsoft.Maui.Automation.Driver.csproj", "{E3CDFDA8-413A-4F0F-B41D-BC8120C637E6}"
|
||||||
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Maui.Automation.Test", "Microsoft.Maui.Automation.Test\Microsoft.Maui.Automation.Test.csproj", "{4CFC183A-CA27-4031-881A-5DAC825D8F67}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestProject1", "TestProject1\TestProject1.csproj", "{1837172F-AB3A-42D2-AC35-79D762ED2554}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -47,6 +53,18 @@ Global
|
||||||
{3B96F274-BD40-44B4-8CC3-C487D278F95F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{3B96F274-BD40-44B4-8CC3-C487D278F95F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{3B96F274-BD40-44B4-8CC3-C487D278F95F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{3B96F274-BD40-44B4-8CC3-C487D278F95F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{3B96F274-BD40-44B4-8CC3-C487D278F95F}.Release|Any CPU.Build.0 = Release|Any CPU
|
{3B96F274-BD40-44B4-8CC3-C487D278F95F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E3CDFDA8-413A-4F0F-B41D-BC8120C637E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{E3CDFDA8-413A-4F0F-B41D-BC8120C637E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E3CDFDA8-413A-4F0F-B41D-BC8120C637E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{E3CDFDA8-413A-4F0F-B41D-BC8120C637E6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{4CFC183A-CA27-4031-881A-5DAC825D8F67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{4CFC183A-CA27-4031-881A-5DAC825D8F67}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4CFC183A-CA27-4031-881A-5DAC825D8F67}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{4CFC183A-CA27-4031-881A-5DAC825D8F67}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{1837172F-AB3A-42D2-AC35-79D762ED2554}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{1837172F-AB3A-42D2-AC35-79D762ED2554}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{1837172F-AB3A-42D2-AC35-79D762ED2554}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{1837172F-AB3A-42D2-AC35-79D762ED2554}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
@ -4,13 +4,23 @@ import public "types.proto";
|
||||||
option csharp_namespace = "Microsoft.Maui.Automation.RemoteGrpc";
|
option csharp_namespace = "Microsoft.Maui.Automation.RemoteGrpc";
|
||||||
|
|
||||||
message ElementsRequest {
|
message ElementsRequest {
|
||||||
Platform platform = 1;
|
string requestId = 1;
|
||||||
string elementId = 2;
|
Platform platform = 2;
|
||||||
optional int32 childDepth = 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message ElementsResponse {
|
message ElementsResponse {
|
||||||
repeated Element elements = 1;
|
string requestId = 1;
|
||||||
|
repeated Element elements = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
message FindElementsRequest {
|
||||||
|
string requestId = 1;
|
||||||
|
Platform platform = 2;
|
||||||
|
string propertyName = 3;
|
||||||
|
string pattern = 4;
|
||||||
|
optional bool isExpression = 5;
|
||||||
|
optional string ancestorId = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
message PropertyRequest {
|
message PropertyRequest {
|
||||||
|
@ -24,6 +34,11 @@ message PropertyResponse {
|
||||||
optional string value = 2;
|
optional string value = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
service RemoteApp {
|
service RemoteApp {
|
||||||
rpc GetElements (ElementsRequest) returns (ElementsResponse);
|
|
||||||
|
// Client calls and reads request objects and streams response objects
|
||||||
|
rpc GetElementsRoute(stream ElementsResponse) returns (stream ElementsRequest) {}
|
||||||
|
rpc FindElementsRoute(stream ElementsResponse) returns (stream FindElementsRequest) {}
|
||||||
|
|
||||||
}
|
}
|
|
@ -22,7 +22,6 @@ message Element {
|
||||||
int32 width = 13;
|
int32 width = 13;
|
||||||
int32 height = 14;
|
int32 height = 14;
|
||||||
|
|
||||||
optional Element parent = 15;
|
|
||||||
repeated Element children = 16;
|
repeated Element children = 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ namespace SampleMauiApp
|
||||||
|
|
||||||
MainPage = new MainPage();
|
MainPage = new MainPage();
|
||||||
|
|
||||||
this.StartAutomationServiceListener();
|
this.StartAutomationServiceListener("http://127.0.0.1:10882");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -34,6 +34,7 @@
|
||||||
Text="Click me"
|
Text="Click me"
|
||||||
FontAttributes="Bold"
|
FontAttributes="Bold"
|
||||||
Grid.Row="3"
|
Grid.Row="3"
|
||||||
|
AutomationId="buttonOne"
|
||||||
SemanticProperties.Hint="Counts the number of times you click"
|
SemanticProperties.Hint="Counts the number of times you click"
|
||||||
Clicked="OnCounterClicked"
|
Clicked="OnCounterClicked"
|
||||||
HorizontalOptions="Center" />
|
HorizontalOptions="Center" />
|
||||||
|
|
|
@ -15,6 +15,7 @@ namespace SampleMauiApp
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return builder.Build();
|
return builder.Build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ namespace RemoteAutomationTests
|
||||||
DefaultPlatform = defaultPlatform;
|
DefaultPlatform = defaultPlatform;
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly List<Element> MockWindows = new ();
|
public readonly List<Element> MockWindows = new();
|
||||||
|
|
||||||
public Element? CurrentMockWindow { get; set; }
|
public Element? CurrentMockWindow { get; set; }
|
||||||
|
|
||||||
|
@ -30,15 +30,36 @@ namespace RemoteAutomationTests
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// return Task.FromResult<IActionResult>(new ActionResult(ActionResultStatus.Error, "No Handler specified."));
|
// return Task.FromResult<IActionResult>(new ActionResult(ActionResultStatus.Error, "No Handler specified."));
|
||||||
|
|
||||||
//}
|
//}
|
||||||
|
|
||||||
public override Task<string?> GetProperty(Platform platform, string elementId, string propertyName)
|
public override Task<string> GetProperty(Platform platform, string elementId, string propertyName)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Task<IEnumerable<Element>> GetElements(Platform platform, string? elementId = null, int depth = 0)
|
public override Task<IEnumerable<Element>> GetElements(Platform platform)
|
||||||
=> Task.FromResult<IEnumerable<Element>>(MockWindows);
|
=> Task.FromResult<IEnumerable<Element>>(MockWindows);
|
||||||
|
|
||||||
|
public override Task<IEnumerable<Element>> FindElements(Platform platform, Func<Element, bool> matcher)
|
||||||
|
{
|
||||||
|
var windows = MockWindows;
|
||||||
|
|
||||||
|
var matches = new List<Element>();
|
||||||
|
Traverse(platform, windows, matches, matcher);
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<Element>>(matches);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Traverse(Platform platform, IEnumerable<Element> elements, IList<Element> matches, Func<Element, bool> matcher)
|
||||||
|
{
|
||||||
|
foreach (var e in elements)
|
||||||
|
{
|
||||||
|
if (matcher(e))
|
||||||
|
matches.Add(e);
|
||||||
|
|
||||||
|
Traverse(platform, e.Children, matches, matcher);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,7 +8,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
using Microsoft.Maui.Automation;
|
using Microsoft.Maui.Automation;
|
||||||
using Microsoft.Maui.Automation.Remote;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
@ -12,62 +11,21 @@ namespace RemoteAutomationTests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ListWindowsTest()
|
public async Task ListWindowsTest()
|
||||||
{
|
{
|
||||||
// Start the 'test runner' as a server, listening for the 'app/device' to connect
|
|
||||||
var tcsHost = new TaskCompletionSource<TcpRemoteApplication>();
|
|
||||||
_ = Task.Run(() =>
|
|
||||||
tcsHost.TrySetResult(new TcpRemoteApplication(Platform.MAUI, IPAddress.Any)));
|
|
||||||
|
|
||||||
// Build a mock app
|
// Build a mock app
|
||||||
var app = new MockApplication()
|
var app = new MockApplication()
|
||||||
.WithWindow("window1", "Window", "Window Title")
|
.WithWindow("window1", "Window", "Window Title")
|
||||||
.WithView("view1");
|
.WithView("view1");
|
||||||
|
|
||||||
|
var windows = new List<Element>();
|
||||||
|
var elems = await app.GetElements(Platform.Maui);
|
||||||
|
|
||||||
// Create our app host service implementation
|
|
||||||
var service = new RemoteAutomationService(app);
|
|
||||||
|
|
||||||
// Connect the 'app/device' as a client
|
|
||||||
var device = new TcpRemoteApplication(Platform.MAUI, IPAddress.Loopback, listen: false, remoteAutomationService: service);
|
|
||||||
|
|
||||||
// Wait until the client connects to the host to proceed
|
|
||||||
var runner = await tcsHost.Task;
|
|
||||||
|
|
||||||
var windows = new List<IElement>();
|
|
||||||
// Query the remote host
|
// Query the remote host
|
||||||
foreach (var window in await runner.Children(Platform.MAUI))
|
foreach (var window in elems)
|
||||||
{
|
{
|
||||||
windows.Add(window);
|
windows.Add(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert.NotEmpty(windows);
|
Assert.NotEmpty(windows);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task IdSelectorTest()
|
|
||||||
{
|
|
||||||
// Start the 'test runner' as a server, listening for the 'app/device' to connect
|
|
||||||
var tcsHost = new TaskCompletionSource<TcpRemoteApplication>();
|
|
||||||
_ = Task.Run(() =>
|
|
||||||
tcsHost.TrySetResult(new TcpRemoteApplication(Platform.MAUI, IPAddress.Any)));
|
|
||||||
|
|
||||||
// Build a mock app
|
|
||||||
var app = new MockApplication()
|
|
||||||
.WithWindow("window1", "Window", "Window Title")
|
|
||||||
.WithView("view1");
|
|
||||||
|
|
||||||
var window = app.CurrentMockWindow;
|
|
||||||
|
|
||||||
// Create our app host service implementation
|
|
||||||
var service = new RemoteAutomationService(app);
|
|
||||||
|
|
||||||
// Connect the 'app/device' as a client
|
|
||||||
var device = new TcpRemoteApplication(Platform.MAUI, IPAddress.Loopback, listen: false, remoteAutomationService: service);
|
|
||||||
|
|
||||||
// Wait until the client connects to the host to proceed
|
|
||||||
var runner = await tcsHost.Task;
|
|
||||||
|
|
||||||
var e = await runner.ById(Platform.MAUI, "view1");
|
|
||||||
|
|
||||||
Assert.NotNull(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
Загрузка…
Ссылка в новой задаче