Merge pull request #13 from Redth/grpc

Move to GRPC
This commit is contained in:
Jonathan Dick 2022-09-03 11:30:13 -04:00 коммит произвёл GitHub
Родитель a4440c1022 029d2c7196
Коммит 4c0ea5036f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
88 изменённых файлов: 1422 добавлений и 2593 удалений

Просмотреть файл

@ -7,14 +7,13 @@ using System.Threading.Tasks;
using Microsoft.Maui.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System.Net;
using Microsoft.Maui.Automation.Remote;
using Grpc.Core;
namespace Microsoft.Maui.Automation
{
public static class AutomationAppBuilderExtensions
{
static IRemoteAutomationService RemoteAutomationService;
static IApplication App;
static GrpcRemoteAppAgent client;
static IApplication CreateApp(
Maui.IApplication app
@ -33,21 +32,17 @@ namespace Microsoft.Maui.Automation
var platform = Automation.App.GetCurrentPlatform();
var multiApp = new MultiPlatformApplication(Platform.MAUI, new[]
var multiApp = new MultiPlatformApplication(Platform.Maui, new[]
{
( Platform.MAUI, new MauiApplication(app)),
( Platform.Maui, new MauiApplication(app)),
( platform, platformApp )
});
return multiApp;
}
public static void StartAutomationServiceConnection(this Maui.IApplication mauiApplication, string host = null, int port = TcpRemoteApplication.DefaultPort)
public static void StartAutomationServiceListener(this Maui.IApplication mauiApplication, string address)
{
IPAddress address = IPAddress.Any;
if (!string.IsNullOrEmpty(host) && IPAddress.TryParse(host, out var ip))
address = ip;
var multiApp = CreateApp(mauiApplication
#if ANDROID
, (Android.App.Application.Context as Android.App.Application)
@ -55,23 +50,8 @@ namespace Microsoft.Maui.Automation
#endif
);
RemoteAutomationService = new RemoteAutomationService(multiApp);
App = new TcpRemoteApplication(Platform.MAUI, address, port, false, RemoteAutomationService);
client = new GrpcRemoteAppAgent(multiApp, address);
}
public static void StartAutomationServiceListener(this Maui.IApplication mauiApplication, int port = TcpRemoteApplication.DefaultPort)
{
var address = IPAddress.Any;
var multiApp = CreateApp(mauiApplication
#if ANDROID
, (Android.App.Application.Context as Android.App.Application)
?? Microsoft.Maui.MauiApplication.Current
#endif
);
RemoteAutomationService = new RemoteAutomationService(multiApp);
App = new TcpRemoteApplication(Platform.MAUI, address, port, true, RemoteAutomationService);
}
}
}
}

Просмотреть файл

@ -14,7 +14,7 @@ namespace Microsoft.Maui.Automation
#endif
)
: base(defaultPlatform, new[] {
( Platform.MAUI, new MauiApplication() ),
( Platform.Maui, new MauiApplication() ),
( App.GetCurrentPlatform(), App.CreateForCurrentPlatform(
#if ANDROID
application

Просмотреть файл

@ -1,4 +1,5 @@
#if IOS || MACCATALYST
using Foundation;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@ -10,48 +11,46 @@ namespace Microsoft.Maui.Automation
{
public class iOSApplication : Application
{
public override Platform DefaultPlatform => Platform.iOS;
public override Platform DefaultPlatform => Platform.Ios;
public override async Task<object> GetProperty(Platform platform, string elementId, string propertyName)
public override async Task<string> GetProperty(Platform platform, string elementId, string propertyName)
{
var p = await base.GetProperty(platform, elementId, propertyName);
if (p != null)
return p;
var selector = new ObjCRuntime.Selector(propertyName);
var getSelector = new ObjCRuntime.Selector("get" + System.Globalization.CultureInfo.InvariantCulture.TextInfo.ToTitleCase(propertyName));
var element = await Element(platform, elementId);
var element = (await FindElements(platform, e => e.Id?.Equals(elementId) ?? false))?.FirstOrDefault();
if (element is iOSView view)
if (element is not null && element.PlatformElement is NSObject nsobj)
{
if (view.PlatformView.RespondsToSelector(selector))
if (nsobj.RespondsToSelector(selector))
{
var v = view.PlatformView.PerformSelector(selector)?.ToString();
var v = nsobj.PerformSelector(selector)?.ToString();
if (v != null)
return v;
}
if (view.PlatformView.RespondsToSelector(getSelector))
if (nsobj.RespondsToSelector(getSelector))
{
var v = view.PlatformView.PerformSelector(getSelector)?.ToString();
var v = nsobj.PerformSelector(getSelector)?.ToString();
if (v != null)
return v;
}
}
return Task.FromResult<object>(null);
return string.Empty;
}
public override Task<IActionResult> Perform(Platform platform, string elementId, IAction action)
public override Task<IEnumerable<Element>> GetElements(Platform platform)
{
throw new NotImplementedException();
var root = GetRootElements(-1);
return Task.FromResult(root);
}
public override Task<IEnumerable<IElement>> Children(Platform platform)
IEnumerable<Element> GetRootElements(int depth)
{
var children = new List<IElement>();
var children = new List<Element>();
var scenes = UIApplication.SharedApplication.ConnectedScenes?.ToArray();
@ -65,7 +64,7 @@ namespace Microsoft.Maui.Automation
{
foreach (var window in windowScene.Windows)
{
children.Add(new iOSWindow(this, window));
children.Add(window.GetElement(this, 1, depth));
hadScenes = true;
}
}
@ -79,12 +78,38 @@ namespace Microsoft.Maui.Automation
{
foreach (var window in UIApplication.SharedApplication.Windows)
{
children.Add(new iOSWindow(this, window));
children.Add(window.GetElement(this, 1, depth));
}
}
}
return Task.FromResult<IEnumerable<IElement>>(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);
}
}
}
}
}

Просмотреть файл

@ -1,51 +1,98 @@
#if IOS || MACCATALYST
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific;
using Microsoft.Maui.Controls.PlatformConfiguration.GTKSpecific;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UIKit;
using static System.Net.Mime.MediaTypeNames;
namespace Microsoft.Maui.Automation
namespace Microsoft.Maui.Automation;
internal static class iOSExtensions
{
internal static class iOSExtensions
static string[] possibleTextPropertyNames = new string[]
{
static string[] possibleTextPropertyNames = new string[]
"Title", "Text",
};
internal static string GetText(this UIView view)
=> view switch
{
"Title", "Text",
IUITextInput ti => TextFromUIInput(ti),
UIButton b => b.CurrentTitle,
_ => TextViaReflection(view, possibleTextPropertyNames)
};
internal static string GetText(this UIView view)
=> view switch
{
IUITextInput ti => TextFromUIInput(ti),
UIButton b => b.CurrentTitle,
_ => TextViaReflection(view, possibleTextPropertyNames)
};
static string TextViaReflection(UIView view, string[] propertyNames)
static string TextViaReflection(UIView view, string[] propertyNames)
{
foreach (var name in propertyNames)
{
foreach (var name in propertyNames)
{
var prop = view.GetType().GetProperty("Text", typeof(string));
if (prop is null)
continue;
if (!prop.CanRead)
continue;
if (prop.PropertyType != typeof(string))
continue;
return prop.GetValue(view) as string ?? "";
}
return "";
var prop = view.GetType().GetProperty("Text", typeof(string));
if (prop is null)
continue;
if (!prop.CanRead)
continue;
if (prop.PropertyType != typeof(string))
continue;
return prop.GetValue(view) as string ?? "";
}
return "";
}
static string TextFromUIInput(IUITextInput ti)
static string TextFromUIInput(IUITextInput ti)
{
var start = ti.BeginningOfDocument;
var end = ti.EndOfDocument;
var range = ti.GetTextRange(start, end);
return ti.TextInRange(range);
}
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 start = ti.BeginningOfDocument;
var end = ti.EndOfDocument;
var range = ti.GetTextRange(start, end);
return ti.TextInRange(range);
AutomationId = uiView.AccessibilityIdentifier,
Visible = !uiView.Hidden,
Enabled = uiView.UserInteractionEnabled,
Focused = uiView.Focused,
X = (int)uiView.Frame.X,
Y = (int)uiView.Frame.Y,
Width = (int)uiView.Frame.Width,
Height = (int)uiView.Frame.Height,
Text = uiView.GetText()
};
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);
}
return e;
}
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)
{
AutomationId = window.AccessibilityIdentifier ?? window.Handle.ToString(),
Width = (int)window.Frame.Width,
Height = (int)window.Frame.Height,
Text = string.Empty
};
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);
}
return e;
}
}
#endif

Просмотреть файл

@ -1,76 +0,0 @@
#if IOS || MACCATALYST
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Drawing;
using System.Linq;
using System.Text.Json.Serialization;
using UIKit;
namespace Microsoft.Maui.Automation
{
public class iOSView : Element
{
public iOSView(IApplication application, UIKit.UIView platformView, string? parentId = null)
: base(application, Platform.iOS, platformView.Handle.ToString(), parentId)
{
PlatformView = platformView;
PlatformElement = platformView;
AutomationId = platformView.AccessibilityIdentifier;
var children = platformView.Subviews?.Select(s => new iOSView(application, s, Id))?.ToList<IElement>() ?? new List<IElement>();
Children = new ReadOnlyCollection<IElement>(children);
Visible = !platformView.Hidden;
Enabled = platformView.UserInteractionEnabled;
Focused = platformView.Focused;
X = (int)platformView.Frame.X;
Y = (int)platformView.Frame.Y;
Width = (int)platformView.Frame.Width;
Height = (int)platformView.Frame.Height;
Text = platformView.GetText();
}
[Newtonsoft.Json.JsonIgnore]
[JsonIgnore]
public UIKit.UIView PlatformView { get; set; }
//public void Clear()
//{
// if (PlatformElement is UIKit.IUITextInput ti)
// {
// var start = ti.BeginningOfDocument;
// var end = ti.EndOfDocument;
// var range = ti.GetTextRange(start, end);
// ti.ReplaceText(range, string.Empty);
// }
//}
//public void SendKeys(string text)
//{
// if (PlatformElement is UIKit.IUITextInput ti)
// {
// var start = ti.BeginningOfDocument;
// var end = ti.EndOfDocument;
// var range = ti.GetTextRange(start, end);
// ti.ReplaceText(range, text);
// }
//}
//public bool Focus()
//{
// if (!PlatformElement.CanBecomeFirstResponder)
// return false;
// return PlatformElement.BecomeFirstResponder();
//}
}
}
#endif

Просмотреть файл

@ -1,32 +0,0 @@
#if IOS || MACCATALYST
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text.Json.Serialization;
using UIKit;
namespace Microsoft.Maui.Automation
{
public class iOSWindow : Element
{
public iOSWindow(IApplication application, UIWindow window)
: base(application, Platform.iOS, window.Handle.ToString())
{
PlatformWindow = window;
PlatformElement = window;
AutomationId = window.AccessibilityIdentifier ?? Id;
var children = window.Subviews?.Select(s => new iOSView(application, s, Id))?.ToList<IElement>() ?? new List<IElement>();
Children = new ReadOnlyCollection<IElement>(children);
Width = (int)PlatformWindow.Frame.Width;
Height = (int)PlatformWindow.Frame.Height;
Text = string.Empty;
}
[Newtonsoft.Json.JsonIgnore]
[JsonIgnore]
public readonly UIWindow PlatformWindow;
}
}
#endif

Просмотреть файл

@ -22,13 +22,13 @@ namespace Microsoft.Maui.Automation
public static Platform GetCurrentPlatform()
=>
#if IOS || MACCATALYST
Platform.iOS;
Platform.Ios;
#elif ANDROID
Platform.Android;
#elif WINDOWS
Platform.WinAppSdk;
Platform.Winappsdk;
#else
Platform.MAUI;
Platform.Maui;
#endif
public static IApplication CreateForCurrentPlatform
@ -47,80 +47,4 @@ namespace Microsoft.Maui.Automation
throw new PlatformNotSupportedException();
#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 async IAsyncEnumerable<IElement> FindDepthFirst(this IAsyncEnumerable<IElement> elements, IElementSelector? selector)
{
var list = new List<IElement>();
await foreach (var e in elements)
list.Add(e);
await foreach (var e in FindDepthFirst(list, selector))
yield return e;
}
internal static async IAsyncEnumerable<IElement> FindDepthFirst(this IEnumerable<IElement> elements, IElementSelector? selector)
{
var st = new Stack<IElement>();
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 async IAsyncEnumerable<IElement> FindBreadthFirst(this IAsyncEnumerable<IElement> elements, IElementSelector? selector)
{
var list = new List<IElement>();
await foreach (var e in elements)
list.Add(e);
await foreach (var e in FindBreadthFirst(list, selector))
yield return e;
}
internal static async IAsyncEnumerable<IElement> FindBreadthFirst(this IEnumerable<IElement> elements, IElementSelector? selector)
{
var q = new Queue<IElement>();
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<IElement> AsReadOnlyCollection(this IElement element)
{
var list = new List<IElement> { element };
return list.AsReadOnly();
}
}
}

Просмотреть файл

@ -1,4 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Maui.Automation.RemoteGrpc;
using Microsoft.Maui.Dispatching;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@ -8,7 +10,7 @@ using System.Threading.Tasks;
namespace Microsoft.Maui.Automation
{
public class MauiApplication : Application
public class MauiApplication : Application
{
public MauiApplication(Maui.IApplication? mauiApp = default) : base()
{
@ -16,54 +18,72 @@ namespace Microsoft.Maui.Automation
?? App.GetCurrentMauiApplication() ?? throw new PlatformNotSupportedException();
}
Task<TResult> Dispatch<TResult>(Func<TResult> action)
{
var tcs = new TaskCompletionSource<TResult>();
Task<TResult> Dispatch<TResult>(Func<Task<TResult>> action)
{
var dispatcher = MauiPlatformApplication.Handler.MauiContext.Services.GetService<Dispatching.IDispatcher>() ?? throw new Exception("Unable to locate Dispatcher");
dispatcher.Dispatch(() =>
return dispatcher.DispatchAsync(action);
}
public override Platform DefaultPlatform => Platform.Maui;
public readonly Maui.IApplication MauiPlatformApplication;
public override Task<IEnumerable<Element>> GetElements(Platform platform)
=> Dispatch<IEnumerable<Element>>(() =>
{
try
{
var r = action();
tcs.TrySetResult(r);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});
return tcs.Task;
}
public override Platform DefaultPlatform => Platform.MAUI;
public readonly Maui.IApplication MauiPlatformApplication;
public override async Task<IEnumerable<IElement>> Children(Platform platform)
{
var windows = await Dispatch(() =>
{
var result = new List<MauiWindow>();
var windows = new List<Element>();
foreach (var window in MauiPlatformApplication.Windows)
{
var w = new MauiWindow(this, window);
result.Add(w);
var w = window.GetMauiElement(this, currentDepth: 1, maxDepth: -1);
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<IActionResult> Perform(Platform platform, string elementId, IAction action)
{
throw new NotImplementedException();
}
}
public override Task<string> GetProperty(Platform platform, string elementId, string propertyName)
{
throw new NotImplementedException();
}
}
}

Просмотреть файл

@ -1,69 +1,130 @@
using System;
using Microsoft.Maui.Controls;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using static System.Net.Mime.MediaTypeNames;
namespace Microsoft.Maui.Automation
{
internal static class MauiExtensions
{
internal static IElement[] GetChildren(this Maui.IWindow window, IApplication application, string? parentId = null)
internal static class MauiExtensions
{
internal static Element[] GetChildren(this Maui.IWindow window, IApplication application, string parentId = "", int currentDepth = -1, int maxDepth = -1)
{
if (window.Content == null)
return Array.Empty<IElement>();
return Array.Empty<Element>();
return new[] { new MauiElement(application, window.Content, parentId) };
return new[] { window.Content.GetMauiElement(application, parentId, currentDepth, maxDepth) };
}
internal static IElement[] GetChildren(this Maui.IView view, IApplication application, string? parentId = null)
internal static Element[] GetChildren(this Maui.IView view, IApplication application, string parentId = "", int currentDepth = -1, int maxDepth = -1)
{
if (view is ILayout layout)
{
var children = new List<IElement>();
{
var children = new List<Element>();
foreach (var v in layout)
{
children.Add(new MauiElement(application, v, parentId));
}
{
children.Add(v.GetMauiElement(application, parentId, currentDepth, maxDepth));
}
return children.ToArray();
}
}
else if (view is IContentView content && content?.Content is Maui.IView contentView)
{
return new[] { new MauiElement(application, contentView, parentId) };
}
{
return new[] { contentView.GetMauiElement(application, parentId, currentDepth, maxDepth) };
}
return Array.Empty<IElement>();
return Array.Empty<Element>();
}
internal static IElement ToAutomationWindow(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 (window.Handler.PlatformView is Android.App.Activity activity)
return new AndroidWindow(application, activity);
return activity.GetElement(application, currentDepth, maxDepth);
#elif IOS || MACCATALYST
if (window.Handler.PlatformView is UIKit.UIWindow uiwindow)
return new iOSWindow(application, uiwindow);
return uiwindow.GetElement(application, currentDepth, maxDepth);
#elif WINDOWS
if (window.Handler.PlatformView is Microsoft.UI.Xaml.Window xamlwindow)
return new WindowsAppSdkWindow(application, xamlwindow);
if (window.Handler.PlatformView is Microsoft.UI.Xaml.Window xamlwindow)
return xamlwindow.GetElement(application, currentDepth, maxDepth);
#endif
return null;
}
internal static IElement ToAutomationView(this Maui.IView view, IApplication application, string? parentId = null)
{
internal static Element GetPlatformElement(this Maui.IView view, IApplication application, string parentId = "", int currentDepth = -1, int maxDepth = -1)
{
#if ANDROID
if (view.Handler.PlatformView is Android.Views.View androidview)
return new AndroidView(application, androidview, parentId);
return androidview.GetElement(application, parentId, currentDepth, maxDepth);
#elif IOS || MACCATALYST
if (view.Handler.PlatformView is UIKit.UIView uiview)
return new iOSView(application, uiview, parentId);
return uiview.GetElement(application, parentId, currentDepth, maxDepth);
#elif WINDOWS
if (view.Handler.PlatformView is Microsoft.UI.Xaml.UIElement uielement)
return new WindowsAppSdkView(application, uielement, parentId);
if (view.Handler.PlatformView is Microsoft.UI.Xaml.UIElement uielement)
return uielement.GetElement(application, parentId, currentDepth, maxDepth);
#endif
return null;
}
internal static Element GetMauiElement(this Maui.IWindow window, IApplication application, string parentId = "", int currentDepth = -1, int maxDepth = -1)
{
var platformElement = window.GetPlatformElement(application);
var e = new Element(application, Platform.Maui, platformElement.Id, window, parentId)
{
Id = platformElement.Id,
AutomationId = platformElement.AutomationId ?? platformElement.Id,
Type = window.GetType().Name,
Width = platformElement.Width,
Height = platformElement.Height,
Text = platformElement.Text ?? ""
};
if (maxDepth <= 0 || (currentDepth + 1 <= maxDepth))
e.Children.AddRange(window.GetChildren(application, e.Id, currentDepth + 1, maxDepth));
return e;
}
internal static Element GetMauiElement(this Maui.IView view, IApplication application, string parentId = "", int currentDepth = -1, int maxDepth = -1)
{
var platformElement = view.GetPlatformElement(application, parentId, currentDepth, maxDepth);
var e = new Element(application, Platform.Maui, platformElement.Id, view, parentId)
{
ParentId = parentId,
AutomationId = view.AutomationId ?? platformElement.Id,
Type = view.GetType().Name,
FullType = view.GetType().FullName,
Visible = view.Visibility == Visibility.Visible,
Enabled = view.IsEnabled,
Focused = view.IsFocused,
X = (int)view.Frame.X,
Y = (int)view.Frame.Y,
Width = (int)view.Frame.Width,
Height = (int)view.Frame.Height,
};
if (view is Microsoft.Maui.IText text && !string.IsNullOrEmpty(text.Text))
e.Text = text.Text;
if (view is Microsoft.Maui.ITextInput input && !string.IsNullOrEmpty(input.Text))
e.Text = input.Text;
if (view is IImage image && image.Source is not null)
e.Text = image.Source?.ToString() ?? "";
if (maxDepth <= 0 || (currentDepth + 1 <= maxDepth))
e.Children.AddRange(view.GetChildren(application, parentId, currentDepth + 1, maxDepth));
return e;
}
}
}

Просмотреть файл

@ -1,71 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Drawing;
using System.Linq;
using System.Text.Json.Serialization;
using Microsoft.Maui;
namespace Microsoft.Maui.Automation
{
public class MauiElement : Element
{
public MauiElement(IApplication application, Maui.IView view, string? parentId = null)
: base(application, Platform.MAUI, string.Empty, parentId)
{
PlatformElement = view;
PlatformView = view.ToAutomationView(application, parentId) ?? throw new PlatformNotSupportedException();
ParentId = parentId;
Id = PlatformView.Id;
AutomationId = PlatformView.AutomationId;
Type = view.GetType().Name;
Visible = PlatformView.Visible;
Enabled = PlatformView.Enabled;
Focused = PlatformView.Focused;
X = PlatformView.X;
X = PlatformView.Y;
Width = PlatformView.Width;
Height = PlatformView.Height;
if (view is Microsoft.Maui.IText text)
Text = text.Text;
if (view is Microsoft.Maui.ITextInput input)
Text = input.Text;
if (view is IImage image)
Text = image.Source?.ToString();
Children = view.GetChildren(application, parentId);
}
[Newtonsoft.Json.JsonIgnore]
[JsonIgnore]
protected IElement PlatformView { get; set; }
// public void Clear()
//{
// if (NativeView is ITextInput ti)
// ti.Text = string.Empty;
//}
//public void SendKeys(string text)
//{
// if (NativeView is ITextInput ti)
// ti.Text = text;
//}
//public bool Focus()
// => PlatformView.Focus();
//public void Click()
//{
// if (NativeView is IButton button)
// button.Clicked();
//}
}
}

Просмотреть файл

@ -1,32 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Microsoft.Maui.Automation
{
public class MauiWindow : Element
{
internal MauiWindow(IApplication application, Maui.IWindow window)
: base(application, Platform.MAUI, "", parentId: null)
{
PlatformWindow = window.ToAutomationWindow(application) ?? throw new PlatformNotSupportedException();
PlatformElement = window;
Id = PlatformWindow.Id;
AutomationId = PlatformWindow.AutomationId;
Type = window.GetType().Name;
Width = PlatformWindow.Width;
Height = PlatformWindow.Height;
Text = PlatformWindow.Text;
Children = window.GetChildren(application, Id);
}
[Newtonsoft.Json.JsonIgnore]
[JsonIgnore]
protected IElement PlatformWindow { get; set; }
}
}

Просмотреть файл

@ -14,13 +14,12 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Maui.Automation.Core\Microsoft.Maui.Automation.Core.csproj" />
<ProjectReference Include="..\Microsoft.Maui.Automation.Remote\Microsoft.Maui.Automation.Remote.csproj" />
<ProjectReference Include="..\Microsoft.Maui.Automation.Core\Microsoft.Maui.Automation.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Platforms\iOS\" />
<Folder Include="Platforms\MacCatalyst\" />
<Folder Include="Platforms\iOS\" />
<Folder Include="Platforms\MacCatalyst\" />
</ItemGroup>
</Project>

Просмотреть файл

@ -1,6 +1,7 @@
using Android.App;
using Android.Content;
using Android.OS;
using Android.Views;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@ -26,11 +27,6 @@ namespace Microsoft.Maui.Automation
AutomationActivityLifecycleContextListener LifecycleListener { get; }
public override Task<IActionResult> Perform(Platform platform, string elementId, IAction action)
{
throw new NotImplementedException();
}
//public override Task<IWindow> CurrentWindow()
//{
// var activity = LifecycleListener.Activity ?? LifecycleListener.Activities.FirstOrDefault();
@ -41,12 +37,44 @@ namespace Microsoft.Maui.Automation
// return Task.FromResult<IWindow>(new AndroidWindow(this, activity));
//}
public override Task<IEnumerable<IElement>> Children(Platform platform)
=> Task.FromResult<IEnumerable<IElement>>(LifecycleListener.Activities.Select(a => new AndroidWindow(this, a)));
public bool IsActivityCurrent(Activity activity)
=> LifecycleListener.Activity == activity;
public override Task<string> GetProperty(Platform platform, string elementId, string propertyName)
{
throw new NotImplementedException();
}
public override Task<IEnumerable<Element>> GetElements(Platform platform)
{
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
{
public readonly List<Activity> Activities = new List<Activity>();

Просмотреть файл

@ -1,37 +1,42 @@
using Android.Views;
using Android.App;
using Android.Content;
using Android.Hardware.Lights;
using Android.Views;
using AndroidX.Core.View.Accessibility;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using static System.Net.Mime.MediaTypeNames;
namespace Microsoft.Maui.Automation
{
public static class AndroidExtensions
{
public static IReadOnlyCollection<IElement> 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<IElement>();
var c = new List<Element>();
if (nativeView is ViewGroup vg)
{
for (int i = 0; i < vg.ChildCount; i++)
c.Add(new AndroidView(application, vg.GetChildAt(i), parentId));
c.Add(vg.GetChildAt(i).GetElement(application, parentId, currentDepth, maxDepth));
}
return new ReadOnlyCollection<IElement>(c.ToList());
return new ReadOnlyCollection<Element>(c.ToList());
}
public static IReadOnlyCollection<IElement> 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 ??
activity.FindViewById(Android.Resource.Id.Content)?.RootView ??
activity.Window?.DecorView?.FindViewById(Android.Resource.Id.Content);
activity.FindViewById(Android.Resource.Id.Content)?.RootView ??
activity.Window?.DecorView?.FindViewById(Android.Resource.Id.Content);
if (rootView is not null)
return new ReadOnlyCollection<IElement>(new List<IElement> { new AndroidView(application, rootView, parentId) });
return new ReadOnlyCollection<Element>(
new List<Element> { rootView.GetElement(application, parentId, currentDepth, maxDepth) });
return new ReadOnlyCollection<IElement>(new List<IElement>());
return new ReadOnlyCollection<Element>(new List<Element>());
}
public static string GetText(this Android.Views.View view)
@ -39,7 +44,7 @@ namespace Microsoft.Maui.Automation
if (view is Android.Widget.TextView tv)
return tv.Text;
return null;
return string.Empty;
}
internal static string EnsureUniqueId(this Android.Views.View view)
@ -85,5 +90,58 @@ namespace Microsoft.Maui.Automation
return rootView.EnsureUniqueId();
}
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)
{
AutomationId = androidview.GetAutomationId(),
Enabled = androidview.Enabled,
Visible = androidview.Visibility == ViewStates.Visible,
Focused = androidview.Selected,
Width = androidview.MeasuredWidth,
Height = androidview.MeasuredHeight,
Text = androidview.GetText(),
};
var loc = new int[2];
androidview?.GetLocationInWindow(loc);
if (loc != null && loc.Length >= 2)
{
e.X = loc[0];
e.Y = loc[1];
}
if (maxDepth <= 0 || (currentDepth + 1 <= maxDepth))
e.Children.AddRange(androidview.GetChildren(e.Application, e.Id, currentDepth + 1, maxDepth));
return e;
}
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)
{
AutomationId = activity.GetAutomationId(),
X = (int)(activity.Window?.DecorView?.GetX() ?? -1f),
Y = (int)(activity.Window?.DecorView?.GetY() ?? -1f),
Width = activity.Window?.DecorView?.Width ?? -1,
Height = activity.Window?.DecorView?.Height ?? -1,
Text = activity.Title
};
if (application is AndroidApplication androidApp)
{
var isCurrent = androidApp.IsActivityCurrent(activity);
e.Visible = isCurrent;
e.Enabled = isCurrent;
e.Focused = isCurrent;
}
if (maxDepth <= 0 || (currentDepth + 1 <= maxDepth))
e.Children.AddRange(activity.GetChildren(e.Application, e.Id, currentDepth + 1, maxDepth));
return e;
}
}
}

Просмотреть файл

@ -1,88 +0,0 @@
using Android.Views;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Text.Json.Serialization;
namespace Microsoft.Maui.Automation
{
public class AndroidView : Element
{
public AndroidView(IApplication application, Android.Views.View platformView, string? parentId = null)
: base(application, Platform.Android, platformView.EnsureUniqueId(), parentId)
{
AutomationId = platformView.GetAutomationId();
Children = platformView.GetChildren(Application, parentId);
Visible = platformView.Visibility == ViewStates.Visible;
Enabled = platformView.Enabled;
Focused = platformView.Selected;
var loc = new int[2];
platformView?.GetLocationInWindow(loc);
if (loc != null && loc.Length >= 2)
{
X = loc[0];
Y = loc[1];
}
Width = platformView.MeasuredWidth;
Height = platformView.MeasuredHeight;
Text = platformView.GetText();
}
[Newtonsoft.Json.JsonIgnore]
[JsonIgnore]
protected Android.Views.View PlatformView { get; set; }
Point Location
{
get
{
var loc = new int[2];
PlatformView.GetLocationInWindow(loc);
if (loc != null && loc.Length >= 2)
return new Point(loc[0], loc[1]);
return Point.Empty;
}
}
//public void Clear()
//{
// if (NativeView is Android.Widget.TextView tv)
// tv.Text = String.Empty;
//}
//public void SendKeys(string text)
//{
// if (NativeView is Android.Widget.TextView tv)
// tv.Text = text;
//}
//public void Return()
//{
// throw new NotImplementedException();
//}
//public bool Focus()
//{
// return NativeView.RequestFocus();
//}
//public void Click()
//{
// NativeView.CallOnClick();
//}
//public string GetProperty(string propertyName)
//{
// // TODO:
// return null;
//}
}
}

Просмотреть файл

@ -1,37 +0,0 @@
using Android.App;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Microsoft.Maui.Automation
{
public class AndroidWindow : Element
{
public AndroidWindow(IApplication application, Activity activity)
: base(application, Platform.Android, activity.GetWindowId())
{
PlatformWindow = activity;
AutomationId = activity.GetAutomationId();
Children = activity.GetChildren(application, Id);
X = (int)(activity.Window?.DecorView?.GetX() ?? -1f);
Y = (int)(activity.Window?.DecorView?.GetY() ?? -1f);
Width = activity.Window?.DecorView?.Width ?? -1;
Height = activity.Window?.DecorView?.Height ?? -1;
Text = PlatformWindow.Title;
if (application is AndroidApplication androidApp)
{
var isCurrent = androidApp.IsActivityCurrent(activity);
Visible = isCurrent;
Enabled = isCurrent;
Focused = isCurrent;
}
}
[JsonIgnore]
protected Activity PlatformWindow { get; set; }
}
}

Просмотреть файл

@ -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.Linq;
using System.Threading.Tasks;
@ -7,50 +10,56 @@ namespace Microsoft.Maui.Automation
{
public class WindowsAppSdkApplication : Application
{
public override Platform DefaultPlatform => Platform.WinAppSdk;
public override Task<IActionResult> Perform(Platform platform, string elementId, IAction action)
=> RunOnMainThreadAsync(() =>
{
// TODO: Handle platform specific actions
return Task.FromResult<IActionResult>(new ActionResult(ActionResultStatus.Unknown));
});
public override async Task<IEnumerable<IElement>> Children(Platform platform)
=> new List<IElement>() { await RunOnMainThreadAsync(() => Task.FromResult(new WindowsAppSdkWindow(this, UI.Xaml.Window.Current))) };
public override Task<IEnumerable<IElement>> Descendants(Platform platform, string ofElementId = null, IElementSelector selector = null)
=> RunOnMainThreadAsync(() => base.Descendants(platform, ofElementId, selector));
public override Platform DefaultPlatform => Platform.Winappsdk;
public override Task<object> GetProperty(Platform platform, string elementId, string propertyName)
=> RunOnMainThreadAsync(() => base.GetProperty(platform, elementId, propertyName));
public override Task<IEnumerable<Element>> GetElements(Platform platform)
=> Task.FromResult<IEnumerable<Element>>(new[] { UI.Xaml.Window.Current.GetElement(this, 1, -1) });
public override Task<IElement> Element(Platform platform, string elementId)
=> RunOnMainThreadAsync(() => base.Element(platform, elementId));
async Task<T> RunOnMainThreadAsync<T>(Func<Task<T>> action)
public override async Task<string> GetProperty(Platform platform, string elementId, string propertyName)
{
var tcs = new TaskCompletionSource<T>();
var matches = await FindElements(platform, e => e.Id?.Equals(elementId) ?? false);
#pragma warning disable VSTHRD101 // Avoid unsupported async delegates
_ = UI.Xaml.Window.Current.Dispatcher.RunAsync(global::Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
{
try
{
tcs.TrySetResult(await action());
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});
#pragma warning restore VSTHRD101 // Avoid unsupported async delegates
var match = matches?.FirstOrDefault();
return await tcs.Task;
if (match is null)
return "";
return match.GetType().GetProperty(propertyName)?.GetValue(match)?.ToString() ?? string.Empty;
}
public override async Task<IEnumerable<Element>> FindElements(Platform platform, Func<Element, bool> matcher)
{
var windows = new[] { UI.Xaml.Window.Current.GetElement(this, 1, 1) };
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);
}
}
}
}
}

Просмотреть файл

@ -1,36 +0,0 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text.Json.Serialization;
namespace Microsoft.Maui.Automation
{
public class WindowsAppSdkView : Element
{
public WindowsAppSdkView(IApplication application, UIElement platformView, string? parentId = null)
: base(application, Platform.WinAppSdk, platformView.GetHashCode().ToString(), parentId)
{
PlatformView = platformView;
PlatformElement = platformView;
AutomationId = platformView.GetType().Name;
var children = (platformView as Panel)?.Children?.Select(c => new WindowsAppSdkView(application, c, Id))?.ToList<IElement>() ?? new List<IElement>();
Children = new ReadOnlyCollection<IElement>(children);
Visible = PlatformView.Visibility == UI.Xaml.Visibility.Visible;
Enabled = PlatformView.IsTapEnabled;
Focused = PlatformView.FocusState != FocusState.Unfocused;
X = (int)PlatformView.ActualOffset.X;
Y = (int)PlatformView.ActualOffset.Y;
Width = (int)PlatformView.ActualSize.X;
Height = (int)PlatformView.ActualSize.Y;
}
[JsonIgnore]
protected UIElement PlatformView { get; set; }
}
}

Просмотреть файл

@ -1,31 +0,0 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Microsoft.Maui.Automation
{
// All the code in this file is only included on Windows.
public class WindowsAppSdkWindow : Element
{
public WindowsAppSdkWindow(IApplication application, Microsoft.UI.Xaml.Window window)
: base(application, Platform.WinAppSdk, window.GetHashCode().ToString())
{
PlatformWindow = window;
AutomationId = Id;
var children = new List<IElement> { new WindowsAppSdkView(application, PlatformWindow.Content, Id) };
Children = new ReadOnlyCollection<IElement>(children);
Width = (int)window.Bounds.Width;
Height = (int)window.Bounds.Height;
Text = window.Title;
}
[Newtonsoft.Json.JsonIgnore]
[JsonIgnore]
public readonly UI.Xaml.Window PlatformWindow;
}
}

Просмотреть файл

@ -0,0 +1,64 @@
using Microsoft.Maui.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Net.Mime.MediaTypeNames;
namespace Microsoft.Maui.Automation;
internal static class WindowsExtensions
{
public static Element GetElement(this UIElement uiElement, IApplication application, string parentId = "", int currentDepth = -1, int maxDepth = -1)
{
var element = new Element(application, Platform.Winappsdk, uiElement.GetHashCode().ToString(), uiElement, parentId)
{
AutomationId = uiElement.GetType().Name,
Visible = uiElement.Visibility == UI.Xaml.Visibility.Visible,
Enabled = uiElement.IsTapEnabled,
Focused = uiElement.FocusState != FocusState.Unfocused,
X = (int)uiElement.ActualOffset.X,
Y = (int)uiElement.ActualOffset.Y,
Width = (int)uiElement.ActualSize.X,
Height = (int)uiElement.ActualSize.Y
};
if (maxDepth <= 0 || (currentDepth + 1 <= maxDepth))
{
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 element;
}
public static Element GetElement(this Microsoft.UI.Xaml.Window window, IApplication application, int currentDepth = -1, int maxDepth = -1)
{
var element = new Element(application, Platform.Winappsdk, window.GetHashCode().ToString(), window)
{
PlatformElement = window,
AutomationId = window.GetType().Name,
X = (int)window.Bounds.X,
Y = (int)window.Bounds.Y,
Width = (int)window.Bounds.Width,
Height = (int)window.Bounds.Height,
Text = window.Title
};
if (maxDepth <= 0 || (currentDepth + 1 <= maxDepth))
{
var c = window.Content.GetElement(application, element.Id, currentDepth + 1, maxDepth);
element.Children.Add(c);
}
return element;
}
}

Просмотреть файл

@ -7,104 +7,61 @@ using System.Threading.Tasks;
namespace Microsoft.Maui.Automation
{
public class ElementNotFoundException : Exception
{
public ElementNotFoundException(string elementId)
: base($"Element with the ID: '{elementId}' was not found.")
{
ElementId = elementId;
}
public class ElementNotFoundException : Exception
{
public ElementNotFoundException(string elementId)
: base($"Element with the ID: '{elementId}' was not found.")
{
ElementId = elementId;
}
public readonly string ElementId;
}
public readonly string ElementId;
}
public abstract class Application : IApplication
{
public virtual void Close()
{
}
public abstract class Application : IApplication
{
public virtual void Close()
{
}
~Application()
{
Dispose(false);
}
~Application()
{
Dispose(false);
}
public virtual void Dispose()
{
Dispose(true);
}
public virtual void Dispose()
{
Dispose(true);
}
bool disposed;
void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
DisposeManagedResources();
DisposeUnmanagedResources();
disposed = true;
}
}
bool disposed;
void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
DisposeManagedResources();
DisposeUnmanagedResources();
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<IEnumerable<IElement>> Children(Platform platform);
public abstract Task<string> GetProperty(Platform platform, string elementId, string propertyName);
public abstract Task<IActionResult> Perform(Platform platform, string elementId, IAction action);
public abstract Task<IEnumerable<Element>> GetElements(Platform platform);
public virtual async Task<object?> GetProperty(Platform platform, string elementId, string propertyName)
{
var element = await Element(platform, elementId);
public abstract Task<IEnumerable<Element>> FindElements(Platform platform, Func<Element, bool> matcher);
var t = element?.PlatformElement?.GetType();
if (t != null)
{
var prop = t.GetProperty(propertyName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (prop != null)
{
return Task.FromResult(prop.GetValue(element?.PlatformElement));
}
}
return Task.FromResult<object?>(null);
}
public virtual async Task<IElement?> Element(Platform platform, string elementId)
{
return (await Children(platform)).FindDepthFirst(new IdSelector(elementId))?.FirstOrDefault();
}
public virtual async Task<IEnumerable<IElement>> Descendants(Platform platform, string? ofElementId = null, IElementSelector? selector = null)
{
var descendants = new List<IElement>();
if (string.IsNullOrEmpty(ofElementId))
{
var children = (await Children(platform))?.FindBreadthFirst(selector);
if (children is not null && children.Any())
descendants.AddRange(children);
}
else
{
var element = await Element(platform, ofElementId);
var children = element?.Children?.FindBreadthFirst(selector);
if (children is not null && children.Any())
descendants.AddRange(children);
}
return descendants;
}
}
}
}

Просмотреть файл

@ -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<IElement>> By(this IElement element, params IElementSelector[] selectors)
=> All(element.Application, element.Platform, selectors);
public static async Task<IElement?> FirstBy(this IElement element, params IElementSelector[] selectors)
=> (await By(element.Application, element.Platform, selectors)).FirstOrDefault();
public static Task<IEnumerable<IElement>> By(this IApplication app, Platform platform, params IElementSelector[] selectors)
=> All(app, platform, selectors);
public static async Task<IElement?> FirstBy(this IApplication app, Platform platform, params IElementSelector[] selectors)
=> (await By(app, platform, selectors))?.FirstOrDefault();
public static Task<IEnumerable<IElement>> ByAutomationId(this IApplication app, Platform platform, string automationId, StringComparison comparison = StringComparison.Ordinal)
=> By(app, platform, new AutomationIdSelector(automationId, comparison));
public static Task<IElement?> ById(this IApplication app, Platform platform, string id, StringComparison comparison = StringComparison.Ordinal)
=> FirstBy(app, platform, new IdSelector(id, comparison));
public static Task<IEnumerable<IElement>> All(this IApplication app, Platform platform, params IElementSelector[] selectors)
=> app.Descendants(platform, selector: new CompoundSelector(any: false, selectors));
public static Task<IEnumerable<IElement>> Any(this IApplication app, Platform platform, params IElementSelector[] selectors)
=> app.Descendants(platform, selector: new CompoundSelector(any: true, selectors));
public static Task<IEnumerable<IElement>> All(this IElement element, Platform platform, params IElementSelector[] selectors)
=> element.Application.Descendants(platform, element.Id, new CompoundSelector(any: false, selectors));
public static Task<IEnumerable<IElement>> Any(this IElement element, Platform platform, params IElementSelector[] selectors)
=> element.Application.Descendants(platform, element.Id, new CompoundSelector(any: true, selectors));
}
}

Просмотреть файл

@ -1,104 +1,35 @@
using System.Collections.ObjectModel;
using System.Text.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace Microsoft.Maui.Automation
{
public abstract class Element : IElement
{
protected Element()
{
Id = Guid.NewGuid().ToString();
Platform = Platform.MAUI;
Application = new NullApplication(Platform);
ParentId = string.Empty;
namespace Microsoft.Maui.Automation;
public partial class Element
{
public Element(IApplication application, Platform platform, string id, object platformElement, string parentId = "")
: base()
{
Application = application;
ParentId = parentId;
Id = id;
AutomationId = Id;
Type = platformElement.GetType().Name;
FullType = platformElement.GetType().FullName ?? Type;
AutomationId = Id;
Type = GetType().Name;
FullType = GetType().FullName ?? Type;
Visible = false;
Enabled = false;
Focused = false;
X = -1;
Y = -1;
Platform = platform;
Width = -1;
Height = -1;
PlatformElement = platformElement;
}
Visible = false;
Enabled = false;
Focused = false;
X = -1;
Y = -1;
Platform = Platform.MAUI;
Children = new ReadOnlyCollection<IElement>(new List<IElement>());
Width = -1;
Height = -1;
}
public Element(IApplication application, Platform platform, string id, string? parentId = null)
{
Application = application;
ParentId = parentId;
Id = id;
AutomationId = Id;
Type = GetType().Name;
FullType = GetType().FullName ?? Type;
Visible = false;
Enabled = false;
Focused = false;
X = -1;
Y = -1;
Platform = platform;
Children = new ReadOnlyCollection<IElement>(new List<IElement>());
Width = -1;
Height = -1;
}
[Newtonsoft.Json.JsonIgnore]
[JsonIgnore]
public virtual IApplication Application { get; protected set; }
[JsonInclude]
public virtual string? ParentId { get; protected set; }
[JsonInclude]
public virtual bool Visible { get; protected set; }
[JsonInclude]
public virtual bool Enabled { get; protected set; }
[JsonInclude]
public virtual bool Focused { get; protected set; }
[JsonInclude]
public virtual int X { get; protected set; }
[JsonInclude]
public virtual int Y { get; protected set; }
[JsonInclude]
public virtual Platform Platform { get; protected set; }
[Newtonsoft.Json.JsonIgnore]
[JsonIgnore]
public virtual object? PlatformElement { get; protected set; }
[JsonInclude]
[Newtonsoft.Json.JsonProperty]
public virtual IReadOnlyCollection<IElement> Children { get; set; }
[JsonInclude]
public virtual string Id { get; protected set; }
[JsonInclude]
public virtual string AutomationId { get; protected set; }
[JsonInclude]
public virtual string Type { get; protected set; }
[JsonInclude]
public virtual string FullType { get; protected set; }
[JsonInclude]
public virtual string? Text { get; protected set; }
[JsonInclude]
public virtual int Width { get; protected set; }
[JsonInclude]
public virtual int Height { get; protected set; }
}
}
public IApplication? Application { get; set; }
public object? PlatformElement { get; set; }
}

Просмотреть файл

@ -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<IElement> FindDepthFirst(this IEnumerable<IElement> elements, IElementSelector? selector)
{
var st = new Stack<IElement>();
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<IElement> FindBreadthFirst(this IEnumerable<IElement> elements, IElementSelector? selector)
{
var q = new Queue<IElement>();
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<IElement> AsReadOnlyCollection(this IElement element)
{
var list = new List<IElement> { element };
return list.AsReadOnly();
}
}
}

Просмотреть файл

@ -2,7 +2,7 @@
{
public interface IAction
{
public Task<IActionResult> Invoke(IElement element);
public Task<IActionResult> Invoke(Element element);
//public void Clear();

Просмотреть файл

@ -1,17 +1,16 @@
namespace Microsoft.Maui.Automation
using Grpc.Core;
using Microsoft.Maui.Automation.RemoteGrpc;
namespace Microsoft.Maui.Automation
{
public interface IApplication
{
public Platform DefaultPlatform { get; }
public Task<IEnumerable<IElement>> Children(Platform platform);
public Task<IEnumerable<Element>> GetElements(Platform platform);
public Task<IElement?> Element(Platform platform, string elementId);
public Task<IEnumerable<IElement>> Descendants(Platform platform, string? ofElementId = null, IElementSelector? selector = null);
public Task<IActionResult> Perform(Platform platform, string elementId, IAction action);
public Task<object?> GetProperty(Platform platform, string elementId, string propertyName);
public Task<IEnumerable<Element>> FindElements(Platform platform, Func<Element, bool> matcher);
public Task<string> GetProperty(Platform platform, string elementId, string propertyName);
}
}

Просмотреть файл

@ -1,41 +0,0 @@

using System.Text.Json.Serialization;
namespace Microsoft.Maui.Automation
{
[JsonConverter(typeof(ElementConverter))]
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; }
[Newtonsoft.Json.JsonIgnore]
[JsonIgnore]
public object? PlatformElement { get; }
[JsonIgnore]
public IReadOnlyCollection<IElement> 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; }
}
}

Просмотреть файл

@ -1,13 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.21.5" />
<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">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Protobuf Include="..\proto\types.proto" />
<Protobuf Include="..\proto\remoteapp.proto" />
</ItemGroup>
</Project>

Просмотреть файл

@ -4,42 +4,39 @@ using System.Threading.Tasks;
namespace Microsoft.Maui.Automation
{
public class MultiPlatformApplication : Application
{
public class MultiPlatformApplication : Application
{
public MultiPlatformApplication(Platform defaultPlatform, params (Platform platform, IApplication app)[] apps)
{
{
DefaultPlatform = defaultPlatform;
PlatformApps = new Dictionary<Platform, IApplication>();
foreach (var app in apps)
PlatformApps[app.platform] = app.app;
}
}
public readonly IDictionary<Platform, IApplication> PlatformApps;
public override Platform DefaultPlatform { get; }
public override Platform DefaultPlatform { get; }
IApplication GetApp(Platform platform)
{
IApplication GetApp(Platform platform)
{
if (!PlatformApps.ContainsKey(platform))
throw new PlatformNotSupportedException();
return PlatformApps[platform];
}
}
public override Task<IEnumerable<IElement>> Children(Platform platform)
=> GetApp(platform).Children(platform);
public override Task<IEnumerable<Element>> GetElements(Platform platform)
=> GetApp(platform).GetElements(platform);
public override Task<IElement?> Element(Platform platform, string elementId)
=> GetApp(platform).Element(platform, elementId);
public override Task<object?> GetProperty(Platform platform, string elementId, string propertyName)
public override Task<string> GetProperty(Platform platform, string elementId, string propertyName)
=> GetApp(platform).GetProperty(platform, elementId, propertyName);
public override Task<IActionResult> Perform(Platform platform, string elementId, IAction action)
=> GetApp(platform).Perform(platform, elementId, action);
}
public override Task<IEnumerable<Element>> FindElements(Platform platform, Func<Element, bool> matcher)
=> GetApp(platform).FindElements(platform, matcher);
}
}

Просмотреть файл

@ -1,31 +0,0 @@
namespace Microsoft.Maui.Automation
{
public class NullApplication : IApplication
{
public NullApplication(Platform platform = Platform.MAUI)
=> DefaultPlatform = platform;
public Platform DefaultPlatform { get; }
public Task<IEnumerable<IElement>> Children(Platform platform)
=> Task.FromResult(Enumerable.Empty<IElement>());
public Task<IEnumerable<IElement>> Descendants(Platform platform, string? ofElementId = null, IElementSelector? selector = null)
=> Task.FromResult(Enumerable.Empty<IElement>());
public Task<IElement?> Element(Platform platform, string elementId)
{
return Task.FromResult<IElement?>(null);
}
public Task<object?> GetProperty(Platform platform, string elementId, string propertyName)
{
return Task.FromResult<object?>(null);
}
public Task<IActionResult> Perform(Platform platform, string elementId, IAction action)
{
return Task.FromResult<IActionResult>(new ActionResult(ActionResultStatus.Unknown));
}
}
}

Просмотреть файл

@ -3,30 +3,29 @@ using System.Text.Json.Serialization;
namespace Microsoft.Maui.Automation
{
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Platform
{
[EnumMember(Value = "MAUI")]
MAUI = 0,
// public enum Platform
// {
// [EnumMember(Value = "MAUI")]
//MAUI = 0,
[EnumMember(Value = "iOS")]
iOS = 1,
// [EnumMember(Value = "iOS")]
// iOS = 100,
[EnumMember(Value = "MacCatalyst")]
MacCatalyst = 2,
// [EnumMember(Value = "MacCatalyst")]
// MacCatalyst = 200,
[EnumMember(Value = "macOS")]
MacOS = 3,
// [EnumMember(Value = "macOS")]
// MacOS = 210,
[EnumMember(Value = "tvOS")]
tvOS = 4,
// [EnumMember(Value = "tvOS")]
// tvOS = 300,
[EnumMember(Value = "Android")]
Android = 10,
// [EnumMember(Value = "Android")]
// Android = 400,
[EnumMember(Value = "WindowsAppSdk")]
WinAppSdk = 20
}
// [EnumMember(Value = "WindowsAppSdk")]
// WinAppSdk = 500
// }
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum ActionResultStatus

Просмотреть файл

@ -1,133 +0,0 @@
// https://gist.github.com/Ilchert/4854a20f790ca963d1ab17d99433c7da
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Microsoft.Maui.Automation
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true)]
public class JsonKnownTypeAttribute : Attribute
{
public Type NestedType { get; }
public string Discriminator { get; }
public JsonKnownTypeAttribute(Type nestedType, string discriminator)
{
NestedType = nestedType;
Discriminator = discriminator;
}
}
public class PolyJsonConverter : JsonConverterFactory
{
private static readonly Type converterType = typeof(PolyJsonConverter<>);
public override bool CanConvert(Type typeToConvert)
{
var attr = typeToConvert.GetCustomAttributes<JsonKnownTypeAttribute>(false);
return attr.Any();
}
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
var attr = typeToConvert.GetCustomAttributes<JsonKnownTypeAttribute>();
var concreteConverterType = converterType.MakeGenericType(typeToConvert);
return (JsonConverter)Activator.CreateInstance(concreteConverterType, attr);
}
}
public class ElementConverter : JsonConverter<IElement>
{
private static readonly JsonEncodedText TypeProperty = JsonEncodedText.Encode("$type");
public override bool CanConvert(Type typeToConvert)
=> typeToConvert.IsAssignableTo(typeof(IElement));
public override IElement? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var doc = JsonDocument.ParseValue(ref reader);
return (IElement)doc.Deserialize(typeToConvert);
}
public override void Write(Utf8JsonWriter writer, IElement value, JsonSerializerOptions options)
{
var type = value.GetType();
writer.WriteStartObject();
writer.WritePropertyName(TypeProperty.EncodedUtf8Bytes);
writer.WriteStringValue(type.FullName);
using var doc = JsonSerializer.SerializeToDocument(value, type, options);
foreach (var prop in doc.RootElement.EnumerateObject())
prop.WriteTo(writer);
writer.WriteEndObject();
}
}
internal class PolyJsonConverter<T> : JsonConverter<T>
{
private readonly Dictionary<string, Type> _discriminatorCache;
private readonly Dictionary<Type, string> _typeCache;
private static readonly JsonEncodedText TypeProperty = JsonEncodedText.Encode("$type");
public PolyJsonConverter(IEnumerable<JsonKnownTypeAttribute> resolvers)
{
_discriminatorCache = resolvers.ToDictionary(p => p.Discriminator, p => p.NestedType);
_typeCache = resolvers.ToDictionary(p => p.NestedType, p => p.Discriminator);
}
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var doc = JsonDocument.ParseValue(ref reader);
if (!doc.RootElement.TryGetProperty(TypeProperty.EncodedUtf8Bytes, out var typeElement))
throw new JsonException();
var discriminator = typeElement.GetString();
if (discriminator is null || !_discriminatorCache.TryGetValue(discriminator, out var type))
throw new JsonException();
return (T)doc.Deserialize(type, options);
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
var type = value.GetType();
if (!_typeCache.TryGetValue(type, out var discriminator))
throw new JsonException();
writer.WriteStartObject();
writer.WritePropertyName(TypeProperty.EncodedUtf8Bytes);
writer.WriteStringValue(discriminator);
using var doc = JsonSerializer.SerializeToDocument(value, type, options);
foreach (var prop in doc.RootElement.EnumerateObject())
prop.WriteTo(writer);
writer.WriteEndObject();
}
}
public class JsonProtectedResolver : Newtonsoft.Json.Serialization.DefaultContractResolver
{
protected override Newtonsoft.Json.Serialization.JsonProperty CreateProperty(MemberInfo member, Newtonsoft.Json.MemberSerialization memberSerialization)
{
var prop = base.CreateProperty(member, memberSerialization);
if (!prop.Writable)
{
var property = member as PropertyInfo;
var hasPrivateSetter = property?.GetSetMethod(true) != null;
prop.Writable = hasPrivateSetter;
}
return prop;
}
}
}

Просмотреть файл

@ -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(IElement 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(IElement 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(IElement view)
=> true;
}
}

Просмотреть файл

@ -1,8 +0,0 @@

namespace Microsoft.Maui.Automation
{
public interface IElementSelector
{
public bool Matches(IElement 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(IElement 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(IElement 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(IElement 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(IElement element)
=> element.Type.Equals(TypeName);
}
}

Просмотреть файл

@ -1,18 +1,18 @@
namespace Microsoft.Maui.Automation
{
public static class ViewExtensions
{
public static bool IsTopLevel(this IElement element)
=> element?.ParentId == element?.Id;
public static class ViewExtensions
{
public static bool IsTopLevel(this Element element)
=> element?.ParentId == element?.Id;
public static string ToString(this IElement element, int depth, int indentSpaces = 2)
{
var v = element;
var t = element.IsTopLevel() ? "window" : "view";
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}]";
}
public static string ToString(this Element element, int depth, int indentSpaces = 2)
{
var v = element;
var t = element.IsTopLevel() ? "window" : "view";
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}]";
}
}
}
}

Просмотреть файл

@ -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>

Просмотреть файл

@ -1,12 +0,0 @@

namespace Microsoft.Maui.Automation.Remote
{
public interface IRemoteAutomationService
{
public Task<RemoteElement[]> Children(Platform platform);
public Task<RemoteElement?> Element(Platform platform, string elementId);
public Task<RemoteElement[]> Descendants(Platform platform, string? elementId = null, IElementSelector? selector = null);
public Task<IActionResult> Perform(Platform platform, string elementId, IAction action);
public Task<object?> GetProperty(Platform platform, string elementId, string propertyName);
}
}

Просмотреть файл

@ -1,20 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Maui.Automation.Core\Microsoft.Maui.Automation.Core.csproj" />
</ItemGroup>
<ItemGroup>
<None Remove="Streamer\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
</Project>

Просмотреть файл

@ -1,96 +0,0 @@
using Streamer;
using System.Collections.ObjectModel;
namespace Microsoft.Maui.Automation.Remote
{
public class RemoteApplication : IApplication
{
public RemoteApplication(Platform defaultPlatform, Stream stream, IRemoteAutomationService? remoteAutomationService = null)
{
DefaultPlatform = defaultPlatform;
Stream = stream;
if (remoteAutomationService != null)
{
RemoteAutomationService = remoteAutomationService;
Server = Streamer.Channel.CreateServer();
Server.Bind(
new MethodHandler<ChildrenResponse, ChildrenRequest>(
nameof(IRemoteAutomationService.Children),
async req => new ChildrenResponse(await RemoteAutomationService.Children(req.Platform))),
new MethodHandler<ElementResponse, ElementRequest>(
nameof(IRemoteAutomationService.Element),
async req => new ElementResponse(await RemoteAutomationService.Element(req.Platform, req.ElementId))),
new MethodHandler<DescendantsResponse, DescendantsRequest>(
nameof(IRemoteAutomationService.Descendants),
async req => new DescendantsResponse(await RemoteAutomationService.Descendants(req.Platform, req.ElementId, req.Selector))),
new MethodHandler<PerformResponse, PerformRequest>(
nameof(IRemoteAutomationService.Perform),
async req => new PerformResponse(await RemoteAutomationService.Perform(req.Platform, req.ElementId, req.Action))),
new MethodHandler<GetPropertyResponse, GetPropertyRequest>(
nameof(IRemoteAutomationService.GetProperty),
async req => new GetPropertyResponse(await RemoteAutomationService.GetProperty(req.Platform, req.ElementId, req.PropertyName)))
);
_ = Task.Run(() => { _ = Server.StartAsync(Stream); });
}
else
{
client = new ClientChannel(Stream);
}
}
public Platform DefaultPlatform { get; }
protected readonly Streamer.ServerChannel? Server;
readonly Streamer.ClientChannel? client;
protected Streamer.ClientChannel Client => client ?? throw new NullReferenceException();
protected readonly IRemoteAutomationService? RemoteAutomationService;
public readonly Stream Stream;
public async Task<IEnumerable<IElement>> Children(Platform platform)
{
try
{
var response = await Client.InvokeAsync<ChildrenRequest, ChildrenResponse>(new ChildrenRequest(platform));
return response?.Result ?? Array.Empty<RemoteElement>();
} catch (Exception ex)
{
Console.WriteLine(ex);
}
return null;
}
public async Task<IElement?> Element(Platform platform, string elementId)
{
var response = await Client.InvokeAsync<ElementRequest, ElementResponse>(new ElementRequest(platform, elementId));
return response?.Element;
}
public async Task<IEnumerable<IElement>> Descendants(Platform platform, string? elementId = null, IElementSelector? selector = null)
{
var response = await Client.InvokeAsync<DescendantsRequest, DescendantsResponse>(new DescendantsRequest(platform, elementId, selector));
return response?.Result ?? Enumerable.Empty<IElement>();
}
public async Task<IActionResult> Perform(Platform platform, string elementId, IAction action)
{
var response = await Client.InvokeAsync<PerformRequest, PerformResponse>(new PerformRequest(platform, elementId, action));
return response?.Result ?? new ActionResult(ActionResultStatus.Error, "Unknown");
}
public async Task<object?> GetProperty(Platform platform, string elementId, string propertyName)
{
var response = await Client.InvokeAsync<GetPropertyRequest, GetPropertyResponse>(new GetPropertyRequest(platform, elementId, propertyName));
return response?.Result;
}
}
}

Просмотреть файл

@ -1,98 +0,0 @@
using Microsoft.Maui.Automation.Remote;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.Maui.Automation
{
public class RemoteAutomationService : IRemoteAutomationService
{
public RemoteAutomationService(IApplication app)
{
PlatformApp = app;
}
readonly IApplication PlatformApp;
public async Task<RemoteElement?> Element(Platform platform, string elementId)
{
var platformElement = await PlatformApp.Element(platform, elementId);
if (platformElement is null)
return null;
return new RemoteElement(PlatformApp, platformElement, platformElement.ParentId);
}
public async Task<RemoteElement[]> Children(Platform platform)
{
var results = new List<RemoteElement>();
var children = await PlatformApp.Children(platform);
foreach (var c in children)
{
var e = new RemoteElement(PlatformApp, c, c.ParentId);
results.Add(e);
}
return results.ToArray();
}
public Task<IActionResult> Perform(Platform platform, string elementId, IAction action)
=> PlatformApp.Perform(platform, elementId, action);
public Task<object?> GetProperty(Platform platform, string elementId, string propertyName)
=> PlatformApp.GetProperty(platform, elementId, propertyName);
void ConvertChildren(RemoteElement parent, IEnumerable<IElement> toConvert, IElementSelector? selector, string? parentId = null)
{
selector ??= new DefaultViewSelector();
var converted = toConvert
.Where(c => selector.Matches(c))
.Select(c => new RemoteElement(PlatformApp, c, parentId)!);
parent.SetChildren(converted);
foreach (var v in converted)
ConvertChildren(v, v.Children, selector);
}
public async Task<RemoteElement[]> Descendants(Platform platform, string? elementId = null, IElementSelector? selector = null)
{
if (!string.IsNullOrEmpty(elementId))
{
var view = await PlatformApp.Element(platform, elementId);
if (view == null)
return Array.Empty<RemoteElement>();
var remoteView = new RemoteElement(PlatformApp, view, view.ParentId);
ConvertChildren(remoteView, remoteView.Children, selector);
var res= remoteView.Children.Cast<RemoteElement>().ToArray();
return res;
}
else
{
var children = new List<RemoteElement>();
foreach (var c in await Children(platform))
{
var remoteView = new RemoteElement(PlatformApp, c, c.ParentId);
ConvertChildren(remoteView, remoteView.Children, selector);
children.Add(c);
}
return children.ToArray();
}
}
}
}

Просмотреть файл

@ -1,48 +0,0 @@
using System.Collections.ObjectModel;
using System.Text.Json.Serialization;
namespace Microsoft.Maui.Automation.Remote
{
public class RemoteElement : Element
{
//[JsonConstructor]
protected RemoteElement() : base() { }
[JsonConstructor]
public RemoteElement(Platform platform = Platform.MAUI)
: base(new NullApplication(platform), platform, "")
{ }
public RemoteElement(IApplication application, IElement from, string? parentId = null)
: base(application, from.Platform, from.Id, parentId)
{
Id = from.Id;
AutomationId = from.AutomationId;
Type = from.Type;
FullType = from.FullType;
Visible = from.Visible;
Enabled = from.Enabled;
Focused = from.Focused;
Text = from.Text;
X = from.X;
Y = from.Y;
Width = from.Width;
Height = from.Height;
var children = from.Children?.Select(c => new RemoteElement(application, c, Id))
?? Enumerable.Empty<RemoteElement>();
Children = new ReadOnlyCollection<IElement>(children.ToList<IElement>());
}
internal void SetChildren(IEnumerable<RemoteElement> children)
{
Children = new ReadOnlyCollection<IElement>(children.ToList<IElement>());
}
}
}

Просмотреть файл

@ -1,16 +0,0 @@
using Microsoft.Maui.Automation;
using Microsoft.Maui.Automation.Remote;
namespace Streamer
{
public class ChildrenRequest : Request
{
public ChildrenRequest(Platform platform)
{
Method = nameof(IRemoteAutomationService.Children);
Platform = platform;
}
public Platform Platform { get; set; }
}
}

Просмотреть файл

@ -1,22 +0,0 @@
using Microsoft.Maui.Automation;
using Microsoft.Maui.Automation.Remote;
namespace Streamer
{
public class DescendantsRequest : Request
{
public DescendantsRequest(Platform platform, string? elementId = null, IElementSelector? selector = null)
{
Method = nameof(IRemoteAutomationService.Descendants);
Platform = platform;
ElementId = elementId;
Selector = selector;
}
public Platform Platform { get; set; }
public string? ElementId { get; set; } = null;
public IElementSelector? Selector { get;}
}
}

Просмотреть файл

@ -1,19 +0,0 @@
using Microsoft.Maui.Automation;
using Microsoft.Maui.Automation.Remote;
namespace Streamer
{
public class ElementRequest : Request
{
public ElementRequest(Platform platform, string elementId)
{
Method = nameof(IRemoteAutomationService.Element);
Platform = platform;
ElementId = elementId;
}
public Platform Platform { get; set; }
public string ElementId { get; set; }
}
}

Просмотреть файл

@ -1,22 +0,0 @@
using Microsoft.Maui.Automation;
using Microsoft.Maui.Automation.Remote;
namespace Streamer
{
public class GetPropertyRequest : Request
{
public GetPropertyRequest(Platform platform, string elementId, string propertyName)
{
Method = nameof(IRemoteAutomationService.GetProperty);
Platform = platform;
ElementId = elementId;
PropertyName = propertyName;
}
public Platform Platform { get; set; }
public string ElementId { get; set; }
public string PropertyName { get; set; }
}
}

Просмотреть файл

@ -1,22 +0,0 @@
using Microsoft.Maui.Automation;
using Microsoft.Maui.Automation.Remote;
namespace Streamer
{
public class PerformRequest : Request
{
public PerformRequest(Platform platform, string elementId, IAction action)
{
Method = nameof(IRemoteAutomationService.Perform);
Platform = platform;
ElementId = elementId;
Action = action;
}
public Platform Platform { get; set; }
public string ElementId { get; set; }
public IAction Action { get; set; }
}
}

Просмотреть файл

@ -1,12 +0,0 @@
using Microsoft.Maui.Automation.Remote;
namespace Streamer
{
public class ChildrenResponse : Response
{
public ChildrenResponse(RemoteElement[] result)
=> Result = result;
public RemoteElement[] Result { get; set; }
}
}

Просмотреть файл

@ -1,14 +0,0 @@
using Microsoft.Maui.Automation.Remote;
namespace Streamer
{
public class DescendantsResponse : Response
{
public DescendantsResponse(RemoteElement[] result)
{
Result = result;
}
public RemoteElement[] Result { get; set; }
}
}

Просмотреть файл

@ -1,14 +0,0 @@
using Microsoft.Maui.Automation.Remote;
namespace Streamer
{
public class ElementResponse : Response
{
public ElementResponse(RemoteElement? result)
{
Element = result;
}
public RemoteElement? Element { get; set; }
}
}

Просмотреть файл

@ -1,11 +0,0 @@

namespace Streamer
{
public class GetPropertyResponse : Response
{
public GetPropertyResponse(object? result)
=> Result = result;
public object? Result { get; set; }
}
}

Просмотреть файл

@ -1,12 +0,0 @@
using Microsoft.Maui.Automation;
namespace Streamer
{
public class PerformResponse : Response
{
public PerformResponse(IActionResult result)
=> Result = result;
public IActionResult Result { get; set; }
}
}

Просмотреть файл

@ -1,125 +0,0 @@
using Microsoft.Maui.Automation;
using System.IO;
using System.Text.Json;
namespace Streamer
{
public class Channel
{
public static ClientChannel CreateClient(Stream stream)
=> new ClientChannel(stream);
public static ServerChannel CreateServer()
=> new ServerChannel();
public static JsonSerializerOptions GetJsonSerializerOptions()
=> new JsonSerializerOptions
{
WriteIndented = true,
Converters = { new PolyJsonConverter(), new ElementConverter() }
};
static Newtonsoft.Json.JsonSerializerSettings GetJsonSerializerSettings()
=> new Newtonsoft.Json.JsonSerializerSettings
{
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.All,
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore,
ContractResolver = new JsonProtectedResolver()
};
public static Response ReadResponse(Stream stream)
{
try
{
// Read length
var msgLenBuffer = new byte[4];
if (stream.Read(msgLenBuffer, 0, 4) != 4)
throw new Exception();
var messageLength = BitConverter.ToInt32(msgLenBuffer);
// Read contents
var messageBuffer = new byte[messageLength];
if (stream.Read(messageBuffer, 0, messageLength) != messageLength)
throw new Exception();
var json = System.Text.Encoding.UTF8.GetString(messageBuffer);
var resp = Newtonsoft.Json.JsonConvert.DeserializeObject<Response>(json, GetJsonSerializerSettings());
return resp;
//return JsonSerializer.Deserialize<Response>(json, GetJsonSerializerOptions());
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
throw ex;
}
}
public static Request ReadRequest(Stream stream)
{
try
{
// Read length
var msgLenBuffer = new byte[4];
if (stream.Read(msgLenBuffer, 0, 4) != 4)
throw new Exception();
var messageLength = BitConverter.ToInt32(msgLenBuffer);
// Read contents
var messageBuffer = new byte[messageLength];
if (stream.Read(messageBuffer, 0, messageLength) != messageLength)
throw new Exception();
var json = System.Text.Encoding.UTF8.GetString(messageBuffer);
return Newtonsoft.Json.JsonConvert.DeserializeObject<Request>(json, GetJsonSerializerSettings());
//return JsonSerializer.Deserialize<Request>(json, GetJsonSerializerOptions());
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
throw ex;
}
}
public static void WriteRequest(Stream stream, Request message)
{
try
{
var json = Newtonsoft.Json.JsonConvert.SerializeObject(message, GetJsonSerializerSettings());
//var json = JsonSerializer.Serialize<Request>(message, GetJsonSerializerOptions());
var data = System.Text.Encoding.UTF8.GetBytes(json);
var msgLen = BitConverter.GetBytes(data.Length);
stream.Write(msgLen, 0, msgLen.Length);
stream.Write(data, 0, data.Length);
stream.Flush();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
throw ex;
}
}
public static void WriteResponse(Stream stream, Response message)
{
try
{
//var json = JsonSerializer.Serialize<Response>(message, GetJsonSerializerOptions());
var json = Newtonsoft.Json.JsonConvert.SerializeObject(message, GetJsonSerializerSettings());
var data = System.Text.Encoding.UTF8.GetBytes(json);
var msgLen = BitConverter.GetBytes(data.Length);
stream.Write(msgLen, 0, msgLen.Length);
stream.Write(data, 0, data.Length);
stream.Flush();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
throw ex;
}
}
}
}

Просмотреть файл

@ -1,111 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Streamer
{
public class ClientChannel : IDisposable
{
private int _id;
private readonly Dictionary<long, Action<Response?>> _invocations = new ();
private readonly Stream _stream;
public ClientChannel(Stream stream)
{
_stream = stream;
new Thread(() => ReadLoop()).Start();
}
public Task<TResponse?> InvokeAsync<TRequest, TResponse>(TRequest request)
where TRequest : Request
where TResponse : Response
{
int id = Interlocked.Increment(ref _id);
request.Id = id;
var tcs = new TaskCompletionSource<TResponse?>();
lock (_invocations)
{
_invocations[id] = response =>
{
try
{
// If there's no response then cancel the call
if (response == null)
{
tcs.TrySetCanceled();
}
else if (response.Error != null)
{
tcs.TrySetException(new InvalidOperationException(response.Error));
}
else
{
tcs.TrySetResult(response as TResponse);
}
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
};
}
Channel.WriteRequest(_stream, request);
return tcs.Task;
}
private void ReadLoop()
{
try
{
while (true)
{
var response = Channel.ReadResponse(_stream);
if (response != null)
{
lock (_invocations)
{
if (_invocations.TryGetValue(response.Id, out var invocation))
{
invocation?.Invoke(response);
_invocations.Remove(response.Id);
}
}
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
// Any pending callbacks need to be cleaned up
lock (_invocations)
{
foreach (var invocation in _invocations)
{
invocation.Value(null);
}
}
}
}
public void Dispose()
{
_stream.Dispose();
}
}
}

Просмотреть файл

@ -1,35 +0,0 @@
namespace Streamer
{
public abstract class MethodHandler
{
public abstract string Name { get; }
public abstract Task<Response> HandleAsync(Request request);
}
public class MethodHandler<TResponse, TRequest> : MethodHandler
where TResponse : Response
where TRequest : Request
{
public MethodHandler(string name, Func<TRequest, Task<TResponse>> handler)
{
Name = name;
Handler = handler;
}
public override string Name { get; }
protected readonly Func<TRequest, Task<TResponse>> Handler;
public override async Task<Response> HandleAsync(Request request)
{
if (request is TRequest typedRequest)
{
return await Handler(typedRequest);
}
throw new ArgumentException($"Invalid request type, expected: {typeof(TRequest).FullName}.", nameof(request));
}
}
}

Просмотреть файл

@ -1,19 +0,0 @@
using Microsoft.Maui.Automation;
namespace Streamer
{
[System.Text.Json.Serialization.JsonConverter(typeof(PolyJsonConverter))]
[JsonKnownType(typeof(ChildrenRequest), nameof(ChildrenRequest))]
[JsonKnownType(typeof(DescendantsRequest), nameof(DescendantsRequest))]
[JsonKnownType(typeof(ElementRequest), nameof(ElementRequest))]
[JsonKnownType(typeof(GetPropertyRequest), nameof(GetPropertyRequest))]
[JsonKnownType(typeof(PerformRequest), nameof(PerformRequest))]
public abstract class Request
{
public int Id { get; set; }
public string? Method { get; set; }
public object?[] Args { get; set; }
}
}

Просмотреть файл

@ -1,18 +0,0 @@

using Microsoft.Maui.Automation;
namespace Streamer
{
[System.Text.Json.Serialization.JsonConverter(typeof(PolyJsonConverter))]
[JsonKnownType(typeof(ChildrenResponse), nameof(ChildrenResponse))]
[JsonKnownType(typeof(DescendantsResponse), nameof(DescendantsResponse))]
[JsonKnownType(typeof(ElementResponse), nameof(ElementResponse))]
[JsonKnownType(typeof(GetPropertyResponse), nameof(GetPropertyResponse))]
[JsonKnownType(typeof(PerformResponse), nameof(PerformResponse))]
public class Response
{
public int Id { get; set; }
public string? Error { get; set; }
}
}

Просмотреть файл

@ -1,127 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Streamer
{
public class ServerChannel
{
private readonly Dictionary<string, Func<Request, Task<Response>>> _callbacks = new Dictionary<string, Func<Request, Task<Response>>>(StringComparer.OrdinalIgnoreCase);
private bool _isBound;
public ServerChannel()
{
}
public IDisposable Bind(params MethodHandler[] methods)
{
if (_isBound)
{
throw new NotSupportedException("Can't bind to different objects");
}
_isBound = true;
foreach (var m in methods)
{
var methodName = m.Name;
if (_callbacks.ContainsKey(methodName))
{
throw new NotSupportedException(String.Format("Duplicate definitions of {0}. Overloading is not supported.", m.Name));
}
_callbacks[methodName] = async request =>
{
Response response = new();
response.Id = request.Id;
try
{
response = await m.HandleAsync(request);
response.Id = request.Id;
}
catch (TargetInvocationException ex)
{
response.Error = ex?.InnerException?.Message ?? ex?.Message;
}
catch (Exception ex)
{
response.Error = ex?.Message;
}
return response;
};
}
return new DisposableAction(() =>
{
foreach (var m in methods)
{
lock (_callbacks)
{
_callbacks.Remove(m.Name);
}
}
});
}
public async Task StartAsync(Stream stream)
{
try
{
while (true)
{
var request = Channel.ReadRequest(stream);
if (request != null)
{
Response? response = null;
if (request.Method != null && _callbacks.TryGetValue(request.Method, out var callback))
{
response = await callback(request);
}
else
{
// If there's no method then return a failed response for this request
response = new Response
{
Id = request.Id,
Error = string.Format("Unknown method '{0}'", request.Method)
};
}
Channel.WriteResponse(stream, response);
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
private class DisposableAction : IDisposable
{
private Action _action;
public DisposableAction(Action action)
{
_action = action;
}
public void Dispose()
{
Interlocked.Exchange(ref _action, () => { }).Invoke();
}
}
}
}

Просмотреть файл

@ -1,54 +0,0 @@
using System.Net;
using System.Net.Sockets;
namespace Microsoft.Maui.Automation.Remote
{
public class TcpRemoteApplication : IApplication
{
public const int DefaultPort = 4327;
public TcpRemoteApplication(Platform defaultPlatform, IPAddress address, int port = DefaultPort, bool listen = true, IRemoteAutomationService? remoteAutomationService = null)
{
DefaultPlatform = defaultPlatform;
if (listen)
{
tcpListener = new TcpListener(address, port);
tcpListener.Start();
client = tcpListener.AcceptTcpClient();
stream = client.GetStream();
remoteApplication = new RemoteApplication(DefaultPlatform, stream, remoteAutomationService);
tcpListener.Stop();
}
else
{
tcpListener = null;
client = new TcpClient();
client.Connect(address, port);
stream = client.GetStream();
remoteApplication = new RemoteApplication(DefaultPlatform, stream, remoteAutomationService);
}
}
readonly Stream stream;
readonly TcpListener? tcpListener;
readonly RemoteApplication remoteApplication;
readonly TcpClient client;
public Platform DefaultPlatform { get; }
public Task<IEnumerable<IElement>> Children(Platform platform)
=> remoteApplication.Children(platform);
public Task<IElement?> Element(Platform platform, string elementId)
=> remoteApplication.Element(platform, elementId);
public Task<IEnumerable<IElement>> Descendants(Platform platform, string? elementId = null, IElementSelector? selector = null)
=> remoteApplication.Descendants(platform, elementId, selector);
public Task<IActionResult> Perform(Platform platform, string elementId, IAction action)
=> remoteApplication.Perform(platform, elementId, action);
public Task<object?> GetProperty(Platform platform, string elementId, string propertyName)
=> remoteApplication.GetProperty(platform, elementId, propertyName);
}
}

Просмотреть файл

@ -1,19 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.44.0" />
</ItemGroup>
<ItemGroup>
<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" />
<ProjectReference Include="..\Microsoft.Maui.Automation.Remote\Microsoft.Maui.Automation.Remote.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Maui.Automation.Core\Microsoft.Maui.Automation.Core.csproj" />
<ProjectReference Include="..\Microsoft.Maui.Automation.Driver\Microsoft.Maui.Automation.Driver.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -1,72 +1,89 @@
using Microsoft.Maui.Automation;
using Grpc.Net.Client;
using Microsoft.Maui.Automation;
using Microsoft.Maui.Automation.Remote;
using Spectre.Console;
using System.Net;
var port = TcpRemoteApplication.DefaultPort;
var address = "http://localhost:10882";
Console.WriteLine($"REPL> Waiting for remote automation connection on port {port}...");
var platform = Platform.MAUI;
var remote = new TcpRemoteApplication(platform, IPAddress.Any, port);
Console.WriteLine($"REPL> Connecting to {address}...");
var platform = Platform.Maui;
var grpc = new GrpcRemoteAppClient();
Console.WriteLine("Connected.");
while(true)
while (true)
{
var input = Console.ReadLine() ?? string.Empty;
var input = Console.ReadLine() ?? string.Empty;
try {
if (input.StartsWith("tree"))
{
var children = await remote.Children(platform);
try
{
if (input.StartsWith("tree"))
{
var children = await grpc.GetElements(platform);
foreach (var w in children)
{
var tree = new Tree(w.ToTable(ConfigureTable));
foreach (var w in children)
{
var tree = new Tree(w.ToTable(ConfigureTable));
var descendants = await remote.Descendants(platform, w.Id);
foreach (var d in descendants)
{
//var node = tree.AddNode(d.ToMarkupString(0, 0));
PrintTree(tree, d, 1);
}
foreach (var d in w.Children)
{
PrintTree(tree, d, 1);
}
AnsiConsole.Write(tree);
}
AnsiConsole.Write(tree);
}
}
else if (input.StartsWith("windows"))
{
foreach (var w in await remote.Children(platform))
{
var tree = new Tree(w.ToTable(ConfigureTable));
}
else if (input.StartsWith("windows"))
{
var children = await grpc.GetElements(platform);
AnsiConsole.Write(tree);
}
}
} catch (Exception ex)
{
Console.WriteLine(ex);
foreach (var w in children)
{
var tree = new Tree(w.ToTable(ConfigureTable));
AnsiConsole.Write(tree);
}
}
else if (input.StartsWith("test"))
{
var elements = await grpc.FindElements(platform, "AutomationId", "buttonOne");
foreach (var w in elements)
{
var tree = new Tree(w.ToTable(ConfigureTable));
AnsiConsole.Write(tree);
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
if (input != null && (input.Equals("quit", StringComparison.OrdinalIgnoreCase)
|| input.Equals("q", StringComparison.OrdinalIgnoreCase)
|| input.Equals("exit", StringComparison.OrdinalIgnoreCase)))
break;
}
if (input != null && (input.Equals("quit", StringComparison.OrdinalIgnoreCase)
|| input.Equals("q", StringComparison.OrdinalIgnoreCase)
|| input.Equals("exit", StringComparison.OrdinalIgnoreCase)))
break;
}
await grpc.Shutdown();
void PrintTree(IHasTreeNodes node, IElement element, int depth)
void PrintTree(IHasTreeNodes node, Element element, int depth)
{
var subnode = node.AddNode(element.ToTable(ConfigureTable));
var subnode = node.AddNode(element.ToTable(ConfigureTable));
foreach (var c in element.Children)
PrintTree(subnode, c, depth);
foreach (var c in element.Children)
PrintTree(subnode, c, depth);
}
static void ConfigureTable(Table table)
{
table.Border(TableBorder.Rounded);
table.Border(TableBorder.Rounded);
}

Просмотреть файл

@ -4,7 +4,7 @@ namespace Microsoft.Maui.Automation
{
public static class ViewExtensions
{
public static Table ToTable(this IElement element, Action<Table>? config = null)
public static Table ToTable(this Element element, Action<Table>? config = null)
{
var table = new Table()
.AddColumn("[bold]" + element.Type + "[/]")

Просмотреть файл

@ -6,9 +6,22 @@
<Nullable>enable</Nullable>
</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>
<ProjectReference Include="..\Microsoft.Maui.Automation.Core\Microsoft.Maui.Automation.Core.csproj" />
<ProjectReference Include="..\Microsoft.Maui.Automation.Remote\Microsoft.Maui.Automation.Remote.csproj" />
<ProjectReference Include="..\Microsoft.Maui.Automation.Driver\Microsoft.Maui.Automation.Driver.csproj" />
</ItemGroup>
</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();
}
}
}

Просмотреть файл

@ -3,19 +3,25 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31724.407
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Maui.Automation.Test", "Microsoft.Maui.Automation.Test\Microsoft.Maui.Automation.Test.csproj", "{6F1E7A5E-B716-4D12-AF3C-FEF4BE0834E1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Maui.Automation.Core", "Microsoft.Maui.Automation.Core\Microsoft.Maui.Automation.Core.csproj", "{BBBCB071-A745-4746-ADB7-59B57983718A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Maui.Automation.AppHost", "Microsoft.Maui.Automation.AppHost\Microsoft.Maui.Automation.AppHost.csproj", "{6D8AD0FF-33A3-46B3-BF24-C87332B6C281}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Maui.Automation.Remote", "Microsoft.Maui.Automation.Remote\Microsoft.Maui.Automation.Remote.csproj", "{426EE334-FFB2-4185-A222-58CB629E5654}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemoteAutomationTests", "tests\RemoteAutomationTests\RemoteAutomationTests.csproj", "{878A4A30-E88E-4C91-9C41-EDF7AB4D1320}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RemoteAutomationTests", "tests\RemoteAutomationTests\RemoteAutomationTests.csproj", "{878A4A30-E88E-4C91-9C41-EDF7AB4D1320}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleMauiApp", "samples\SampleMauiApp\SampleMauiApp.csproj", "{AFB868DD-DAA2-4002-A9FE-A2781E915A95}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleMauiApp", "samples\SampleMauiApp\SampleMauiApp.csproj", "{AFB868DD-DAA2-4002-A9FE-A2781E915A95}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Maui.Automation.Repl", "Microsoft.Maui.Automation.Repl\Microsoft.Maui.Automation.Repl.csproj", "{3B96F274-BD40-44B4-8CC3-C487D278F95F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Maui.Automation.Repl", "Microsoft.Maui.Automation.Repl\Microsoft.Maui.Automation.Repl.csproj", "{3B96F274-BD40-44B4-8CC3-C487D278F95F}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Proto", "Proto", "{99757F39-4185-4511-9D9C-527D2D35B37C}"
ProjectSection(SolutionItems) = preProject
proto\remoteapp.proto = proto\remoteapp.proto
proto\types.proto = proto\types.proto
EndProjectSection
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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -23,10 +29,6 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6F1E7A5E-B716-4D12-AF3C-FEF4BE0834E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6F1E7A5E-B716-4D12-AF3C-FEF4BE0834E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F1E7A5E-B716-4D12-AF3C-FEF4BE0834E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F1E7A5E-B716-4D12-AF3C-FEF4BE0834E1}.Release|Any CPU.Build.0 = Release|Any CPU
{BBBCB071-A745-4746-ADB7-59B57983718A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BBBCB071-A745-4746-ADB7-59B57983718A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BBBCB071-A745-4746-ADB7-59B57983718A}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -35,10 +37,6 @@ Global
{6D8AD0FF-33A3-46B3-BF24-C87332B6C281}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6D8AD0FF-33A3-46B3-BF24-C87332B6C281}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6D8AD0FF-33A3-46B3-BF24-C87332B6C281}.Release|Any CPU.Build.0 = Release|Any CPU
{426EE334-FFB2-4185-A222-58CB629E5654}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{426EE334-FFB2-4185-A222-58CB629E5654}.Debug|Any CPU.Build.0 = Debug|Any CPU
{426EE334-FFB2-4185-A222-58CB629E5654}.Release|Any CPU.ActiveCfg = Release|Any CPU
{426EE334-FFB2-4185-A222-58CB629E5654}.Release|Any CPU.Build.0 = Release|Any CPU
{878A4A30-E88E-4C91-9C41-EDF7AB4D1320}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{878A4A30-E88E-4C91-9C41-EDF7AB4D1320}.Debug|Any CPU.Build.0 = Debug|Any CPU
{878A4A30-E88E-4C91-9C41-EDF7AB4D1320}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -53,6 +51,14 @@ Global
{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.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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

44
proto/remoteapp.proto Normal file
Просмотреть файл

@ -0,0 +1,44 @@
syntax = "proto3";
import public "types.proto";
option csharp_namespace = "Microsoft.Maui.Automation.RemoteGrpc";
message ElementsRequest {
string requestId = 1;
Platform platform = 2;
}
message ElementsResponse {
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 {
Platform platform = 1;
string elementId = 2;
string propertyName = 3;
}
message PropertyResponse {
Platform platform = 1;
optional string value = 2;
}
service RemoteApp {
// Client calls and reads request objects and streams response objects
rpc GetElementsRoute(stream ElementsResponse) returns (stream ElementsRequest) {}
rpc FindElementsRoute(stream ElementsResponse) returns (stream FindElementsRequest) {}
}

36
proto/types.proto Normal file
Просмотреть файл

@ -0,0 +1,36 @@
syntax = "proto3";
option csharp_namespace = "Microsoft.Maui.Automation";
message Element {
string id = 1;
optional string parentId = 2;
optional string automationId = 3;
Platform platform = 4;
string type = 5;
string fullType = 6;
optional string text = 7;
bool visible = 8;
bool enabled = 9;
bool focused = 10;
int32 x = 11;
int32 y = 12;
int32 width = 13;
int32 height = 14;
repeated Element children = 16;
}
enum Platform {
MAUI = 0;
IOS = 100;
MACCATALYST = 200;
MACOS = 210;
TVOS = 300;
ANDROID = 400;
WINAPPSDK = 500;
}

Просмотреть файл

@ -11,9 +11,7 @@ namespace SampleMauiApp
MainPage = new MainPage();
this.StartAutomationServiceConnection("127.0.0.1");
this.StartAutomationServiceListener("http://127.0.0.1:10882");
}
}
}

Просмотреть файл

@ -34,6 +34,7 @@
Text="Click me"
FontAttributes="Bold"
Grid.Row="3"
AutomationId="buttonOne"
SemanticProperties.Hint="Counts the number of times you click"
Clicked="OnCounterClicked"
HorizontalOptions="Center" />

Просмотреть файл

@ -15,6 +15,7 @@ namespace SampleMauiApp
});
return builder.Build();
}
}

Просмотреть файл

@ -42,7 +42,6 @@
<ItemGroup>
<ProjectReference Include="..\..\Microsoft.Maui.Automation.AppHost\Microsoft.Maui.Automation.AppHost.csproj" />
<ProjectReference Include="..\..\Microsoft.Maui.Automation.Core\Microsoft.Maui.Automation.Core.csproj" />
<ProjectReference Include="..\..\Microsoft.Maui.Automation.Remote\Microsoft.Maui.Automation.Remote.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -3,36 +3,63 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace RemoteAutomationTests
{
public class MockApplication : Application
{
public MockApplication(Platform defaultPlatform = Platform.MAUI) : base()
public MockApplication(Platform defaultPlatform = Platform.Maui) : base()
{
DefaultPlatform = defaultPlatform;
}
public readonly List<MockWindow> MockWindows = new ();
public readonly List<Element> MockWindows = new();
public MockWindow? CurrentMockWindow { get; set; }
public Element? CurrentMockWindow { get; set; }
public override Platform DefaultPlatform { get; }
public override Task<IEnumerable<IElement>> Children(Platform platform)
=> Task.FromResult<IEnumerable<IElement>>(MockWindows);
public Func<Platform, string, IAction, Task<IActionResult>>? PerformHandler { get; set; }
public override Task<IActionResult> Perform(Platform platform, string elementId, IAction action)
{
if (PerformHandler is not null)
{
return PerformHandler.Invoke(platform, elementId, action);
}
//public override Task<IActionResult> Perform(Platform platform, string elementId, IAction action)
//{
// if (PerformHandler is not null)
// {
// return PerformHandler.Invoke(platform, elementId, action);
// }
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)
{
throw new NotImplementedException();
}
public override Task<IEnumerable<Element>> GetElements(Platform platform)
=> 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);
}
}
}
}

Просмотреть файл

@ -1,8 +1,15 @@
namespace RemoteAutomationTests
using Microsoft.Maui.Automation;
namespace RemoteAutomationTests
{
public class MockWindow
{
public string Title { get; set; }
}
public static class MockApplicationExtensions
{
public static MockApplication WithWindow(this MockApplication app, MockWindow window)
public static MockApplication WithWindow(this MockApplication app, Element window)
{
app.MockWindows.Add(window);
return app;
@ -10,22 +17,24 @@
public static MockApplication WithWindow(this MockApplication app, string id, string? automationId, string? title)
{
var w = new MockWindow(app, app.DefaultPlatform, id, automationId, title);
var w = new Element(app, app.DefaultPlatform, id, new MockWindow());
w.Text = title;
app.MockWindows.Add(w);
app.CurrentMockWindow = w;
return app;
}
public static MockApplication WithView(this MockApplication app, MockView view)
public static MockApplication WithView(this MockApplication app, Element view)
{
app.CurrentMockWindow!.MockViews.Add(view);
app.CurrentMockWindow!.Children.Add(view);
return app;
}
public static MockApplication WithView(this MockApplication app, string id)
{
var window = app.CurrentMockWindow!;
window.MockViews.Add(new MockView(app, app.DefaultPlatform, window.Id, id));
window.Children.Add(new Element(app, app.DefaultPlatform, id, new MockWindow(), window.Id));
return app;
}
}

Просмотреть файл

@ -1,12 +0,0 @@
namespace RemoteAutomationTests
{
public class MockNativeView
{
}
public class MockNativeWindow
{
}
}

Просмотреть файл

@ -1,24 +0,0 @@
using Microsoft.Maui.Automation;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace RemoteAutomationTests
{
public class MockView : Element
{
public MockView(IApplication application, Platform platform, string id, string? parentId = null)
: base(application, platform, id, parentId)
{
Text = string.Empty;
PlatformElement = new MockNativeView();
}
public readonly List<MockView> MockViews = new List<MockView>();
public override IReadOnlyCollection<IElement> Children
=> new ReadOnlyCollection<IElement>(MockViews.ToList<IElement>());
}
}

Просмотреть файл

@ -1,44 +0,0 @@
using Microsoft.Maui.Automation;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace RemoteAutomationTests
{
public class MockWindow : Element
{
public MockWindow(IApplication application, Platform platform, string id, string? automationId, string? title = null)
: base(application, platform, id)
{
AutomationId = automationId ?? Id;
Text = title ?? string.Empty;
PlatformElement = new MockNativeWindow();
}
public readonly List<MockView> MockViews = new List<MockView>();
Platform platform;
public override Platform Platform => platform;
public override IReadOnlyCollection<IElement> Children
=> new ReadOnlyCollection<IElement>(MockViews.ToList<IElement>());
}
public static class MockWindowExtensions
{
public static MockWindow WithView(this MockWindow window, MockView view)
{
window.MockViews.Add(view);
return window;
}
public static MockWindow WithView(this MockWindow window, string windowId, string id)
{
window.MockViews.Add(new MockView(window.Application, window.Platform, windowId, id));
return window;
}
}
}

Просмотреть файл

@ -8,7 +8,7 @@
</PropertyGroup>
<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.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -23,8 +23,6 @@
<ItemGroup>
<ProjectReference Include="..\..\Microsoft.Maui.Automation.AppHost\Microsoft.Maui.Automation.AppHost.csproj" />
<ProjectReference Include="..\..\Microsoft.Maui.Automation.Core\Microsoft.Maui.Automation.Core.csproj" />
<ProjectReference Include="..\..\Microsoft.Maui.Automation.Remote\Microsoft.Maui.Automation.Remote.csproj" />
<ProjectReference Include="..\..\Microsoft.Maui.Automation.Test\Microsoft.Maui.Automation.Test.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -1,5 +1,4 @@
using Microsoft.Maui.Automation;
using Microsoft.Maui.Automation.Remote;
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
@ -12,62 +11,21 @@ namespace RemoteAutomationTests
[Fact]
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
var app = new MockApplication()
.WithWindow("window1", "Window", "Window Title")
.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
foreach (var window in await runner.Children(Platform.MAUI))
foreach (var window in elems)
{
windows.Add(window);
}
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);
}
}
}