From a1e29e4c5a6dd884f29f37598fda70791454edc9 Mon Sep 17 00:00:00 2001 From: redth Date: Wed, 7 Sep 2022 16:26:45 -0400 Subject: [PATCH] Refactorings --- .../AppBuilderExtensions.cs | 30 +--- .../AppHostApplication.cs | 52 +++--- .../Apple/iOSApplication.cs | 16 +- .../MauiApplication.cs | 59 +++++-- .../Platforms/Android/AndroidApplication.cs | 40 ++--- .../Platforms/Android/AndroidExtensions.cs | 43 +++++ .../Windows/WindowsAppSdkApplication.cs | 16 +- Microsoft.Maui.Automation.Core/Application.cs | 14 +- .../ElementExtensions.cs | 71 ++++---- .../GrpcRemoteAppAgent.cs | 120 +++++++------ .../IApplicationInterop.cs | 13 +- .../MultiPlatformApplication.cs | 16 +- .../PerformActionResult.cs | 14 +- .../AndroidDriver.cs | 39 +++-- Microsoft.Maui.Automation.Driver/AppDriver.cs | 60 ++++--- .../AutomationConfiguration.cs | 20 ++- Microsoft.Maui.Automation.Driver/Driver.cs | 77 +++++++++ .../DriverExtensions.cs | 38 +++++ Microsoft.Maui.Automation.Driver/GrpcHost.cs | 74 ++++++--- .../GrpcRemoteAppClient.cs | 12 +- .../IAutomationConfiguration.cs | 3 + Microsoft.Maui.Automation.Driver/IDriver.cs | 17 +- .../WindowsDriver.cs | 157 ++++++++++-------- Microsoft.Maui.Automation.Driver/iOSDriver.cs | 26 ++- Microsoft.Maui.Automation.Repl/Program.cs | 121 +++++++++----- Microsoft.Maui.Automation.Test/Tests.cs | 42 ++--- samples/SampleMauiApp/App.xaml.cs | 2 +- samples/SampleMauiApp/MainPage.xaml | 1 + 28 files changed, 744 insertions(+), 449 deletions(-) create mode 100644 Microsoft.Maui.Automation.Driver/Driver.cs create mode 100644 Microsoft.Maui.Automation.Driver/DriverExtensions.cs diff --git a/Microsoft.Maui.Automation.AppHost/AppBuilderExtensions.cs b/Microsoft.Maui.Automation.AppHost/AppBuilderExtensions.cs index bddf3b3..d80f2a1 100644 --- a/Microsoft.Maui.Automation.AppHost/AppBuilderExtensions.cs +++ b/Microsoft.Maui.Automation.AppHost/AppBuilderExtensions.cs @@ -35,7 +35,7 @@ namespace Microsoft.Maui.Automation var multiApp = new MultiPlatformApplication(Platform.Maui, new[] { - ( Platform.Maui, new MauiApplication(app)), + ( Platform.Maui, new MauiApplication(platformApp, app)), ( platform, platformApp ) }); @@ -51,36 +51,8 @@ namespace Microsoft.Maui.Automation #endif ); -#if ANDROID - client = new GrpcRemoteAppAgent(multiApp, address, new MyAndroidHandler()); -#else client = new GrpcRemoteAppAgent(multiApp, address); -#endif } } - -#if ANDROID - public class MyAndroidHandler : Xamarin.Android.Net.AndroidMessageHandler - { - MyHostnameVerifier verifier = new MyHostnameVerifier(); - - protected override Javax.Net.Ssl.IHostnameVerifier GetSSLHostnameVerifier(Javax.Net.Ssl.HttpsURLConnection connection) - { - return verifier; - } - protected override Javax.Net.Ssl.SSLSocketFactory ConfigureCustomSSLSocketFactory(Javax.Net.Ssl.HttpsURLConnection connection) - { - return Android.Net.SSLCertificateSocketFactory.GetInsecure(1000, null); - } - - class MyHostnameVerifier : Java.Lang.Object, Javax.Net.Ssl.IHostnameVerifier - { - public bool Verify(string hostname, Javax.Net.Ssl.ISSLSession session) - { - return true; - } - } - } -#endif } diff --git a/Microsoft.Maui.Automation.AppHost/AppHostApplication.cs b/Microsoft.Maui.Automation.AppHost/AppHostApplication.cs index dbb7c97..a7b175c 100644 --- a/Microsoft.Maui.Automation.AppHost/AppHostApplication.cs +++ b/Microsoft.Maui.Automation.AppHost/AppHostApplication.cs @@ -1,28 +1,28 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; +//using System; +//using System.Collections.Generic; +//using System.Threading.Tasks; -namespace Microsoft.Maui.Automation -{ - public class AppHostApplication : MultiPlatformApplication - { - public AppHostApplication - ( - Platform defaultPlatform -#if ANDROID - , Android.App.Application application -#endif - ) - : base(defaultPlatform, new[] { - ( Platform.Maui, new MauiApplication() ), - ( App.GetCurrentPlatform(), App.CreateForCurrentPlatform( -#if ANDROID - application -#endif - )) - }) - { - } - } -} +//namespace Microsoft.Maui.Automation +//{ +// public class AppHostApplication : MultiPlatformApplication +// { +// public AppHostApplication +// ( +// Platform defaultPlatform +//#if ANDROID +// , Android.App.Application application +//#endif +// ) +// : base(defaultPlatform, new[] { +// ( Platform.Maui, new MauiApplication() ), +// ( App.GetCurrentPlatform(), App.CreateForCurrentPlatform( +//#if ANDROID +// application +//#endif +// )) +// }) +// { +// } +// } +//} diff --git a/Microsoft.Maui.Automation.AppHost/Apple/iOSApplication.cs b/Microsoft.Maui.Automation.AppHost/Apple/iOSApplication.cs index 5c8f8c0..369aaa3 100644 --- a/Microsoft.Maui.Automation.AppHost/Apple/iOSApplication.cs +++ b/Microsoft.Maui.Automation.AppHost/Apple/iOSApplication.cs @@ -13,12 +13,12 @@ namespace Microsoft.Maui.Automation { public override Platform DefaultPlatform => Platform.Ios; - public override async Task GetProperty(Platform platform, string elementId, string propertyName) + public override async Task GetProperty(string elementId, string propertyName) { var selector = new ObjCRuntime.Selector(propertyName); var getSelector = new ObjCRuntime.Selector("get" + System.Globalization.CultureInfo.InvariantCulture.TextInfo.ToTitleCase(propertyName)); - var element = (await FindElements(platform, e => e.Id?.Equals(elementId) ?? false))?.FirstOrDefault(); + var element = (await FindElements(e => e.Id?.Equals(elementId) ?? false))?.FirstOrDefault(); if (element is not null && element.PlatformElement is NSObject nsobj) { @@ -40,7 +40,7 @@ namespace Microsoft.Maui.Automation return string.Empty; } - public override Task> GetElements(Platform platform) + public override Task> GetElements() { var root = GetRootElements(-1); @@ -86,20 +86,20 @@ namespace Microsoft.Maui.Automation return children; } - public override Task> FindElements(Platform platform, Func matcher) + public override Task> FindElements(Func matcher) { var windows = GetRootElements(-1); var matches = new List(); - Traverse(platform, windows, matches, matcher); + Traverse(windows, matches, matcher); return Task.FromResult>(matches); } - public override Task PerformAction(Platform platform, string action, string elementId, params string[] arguments) + public override Task PerformAction(string action, string elementId, params string[] arguments) => Task.FromResult(new PerformActionResult { Result = String.Empty, Status = -1 }); - void Traverse(Platform platform, IEnumerable elements, IList matches, Func matcher) + void Traverse(IEnumerable elements, IList matches, Func matcher) { foreach (var e in elements) { @@ -110,7 +110,7 @@ namespace Microsoft.Maui.Automation { var children = uiView.Subviews?.Select(s => s.GetElement(this, e.Id, 1, 1)) ?.ToList() ?? new List(); - Traverse(platform, children, matches, matcher); + Traverse(children, matches, matcher); } } } diff --git a/Microsoft.Maui.Automation.AppHost/MauiApplication.cs b/Microsoft.Maui.Automation.AppHost/MauiApplication.cs index 1a7cc83..80d7e43 100644 --- a/Microsoft.Maui.Automation.AppHost/MauiApplication.cs +++ b/Microsoft.Maui.Automation.AppHost/MauiApplication.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Maui.Automation.RemoteGrpc; +using Microsoft.Maui.Controls; using Microsoft.Maui.Dispatching; +using Microsoft.Maui.Platform; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -12,8 +14,10 @@ namespace Microsoft.Maui.Automation { public class MauiApplication : Application { - public MauiApplication(Maui.IApplication? mauiApp = default) : base() + public MauiApplication(IApplication platformApp, Maui.IApplication mauiApp = default) : base() { + PlatformApplication = platformApp; + MauiPlatformApplication = mauiApp ?? App.GetCurrentMauiApplication() ?? throw new PlatformNotSupportedException(); } @@ -29,7 +33,9 @@ namespace Microsoft.Maui.Automation public readonly Maui.IApplication MauiPlatformApplication; - public override Task> GetElements(Platform platform) + public readonly IApplication PlatformApplication; + + public override Task> GetElements() => Dispatch>(() => { var windows = new List(); @@ -43,7 +49,7 @@ namespace Microsoft.Maui.Automation return Task.FromResult>(windows); }); - public override Task> FindElements(Platform platform, Func matcher) + public override Task> FindElements(Func matcher) => Dispatch>(() => { var windows = new List(); @@ -55,12 +61,12 @@ namespace Microsoft.Maui.Automation } var matches = new List(); - Traverse(platform, windows, matches, matcher); + Traverse(windows, matches, matcher); return Task.FromResult>(matches); }); - void Traverse(Platform platform, IEnumerable elements, IList matches, Func matcher) + void Traverse(IEnumerable elements, IList matches, Func matcher) { foreach (var e in elements) { @@ -70,25 +76,50 @@ namespace Microsoft.Maui.Automation if (e.PlatformElement is IView view) { var children = view.GetChildren(this, e.Id, 1, 1); - Traverse(platform, children, matches, matcher); + Traverse(children, matches, matcher); } else if (e.PlatformElement is IWindow window) { var children = window.GetChildren(this, e.Id, 1, 1); - Traverse(platform, children, matches, matcher); + Traverse(children, matches, matcher); } } } - public override Task GetProperty(Platform platform, string elementId, string propertyName) + public override async Task GetProperty(string elementId, string propertyName) { - throw new NotImplementedException(); + var element = await this.FirstById(elementId); + + return element.PlatformElement + .GetType() + .GetProperty(propertyName, System.Reflection.BindingFlags.GetProperty | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic) + ?.GetValue(element.Platform)?.ToString() ?? String.Empty; } - public override Task PerformAction(Platform platform, string action, string elementId, params string[] arguments) - { - throw new NotImplementedException(); - } - } + public override Task PerformAction(string action, string elementId, params string[] arguments) + => action switch + { + Actions.Tap => PerformTap(elementId, arguments), + _ => throw new NotImplementedException() + }; + + async Task PerformTap(string elementId, params string[] arguments) + { + if (!string.IsNullOrEmpty(elementId)) + { + var element = await this.FirstById(elementId); + + if (element.PlatformElement is IElement mauiElement) + { +#if ANDROID + if (mauiElement.Handler?.PlatformView is Android.Views.View androidView) + return await androidView.PerformAction(Actions.Tap, elementId, arguments); +#endif + } + } + + throw new NotImplementedException(); + } + } } diff --git a/Microsoft.Maui.Automation.AppHost/Platforms/Android/AndroidApplication.cs b/Microsoft.Maui.Automation.AppHost/Platforms/Android/AndroidApplication.cs index 849b3c1..fa320c4 100644 --- a/Microsoft.Maui.Automation.AppHost/Platforms/Android/AndroidApplication.cs +++ b/Microsoft.Maui.Automation.AppHost/Platforms/Android/AndroidApplication.cs @@ -40,52 +40,36 @@ namespace Microsoft.Maui.Automation public bool IsActivityCurrent(Activity activity) => LifecycleListener.Activity == activity; - public override Task GetProperty(Platform platform, string elementId, string propertyName) + public override Task GetProperty(string elementId, string propertyName) { throw new NotImplementedException(); } - public override Task> GetElements(Platform platform) + public override Task> GetElements() { return Task.FromResult(LifecycleListener.Activities.Select(a => a.GetElement(this, 1, -1))); } - public override Task> FindElements(Platform platform, Func matcher) + public override Task> FindElements(Func matcher) { var windows = LifecycleListener.Activities.Select(a => a.GetElement(this, 1, 1)); var matches = new List(); - Traverse(platform, windows, matches, matcher); + Traverse(windows, matches, matcher); return Task.FromResult>(matches); } - public override async Task PerformAction(Platform platform, string action, string elementId, params string[] arguments) - { - if (action == Actions.Tap) - { - if (!string.IsNullOrEmpty(elementId)) - { - var element = await this.FirstById(elementId); - if (element.PlatformElement is View androidView) - { - var r = androidView.PerformClick(); - return PerformActionResult.Ok(); - } - } - else if (arguments is not null - && arguments.Length == 2 - && ulong.TryParse(arguments[0], out var x) - && ulong.TryParse(arguments[1], out var y)) - { - // tap x and y - } - } + public override async Task PerformAction(string action, string elementId, params string[] arguments) + { + var element = await this.FirstById(elementId); + if (element?.PlatformElement is Android.Views.View androidView) + return await androidView.PerformAction(action, elementId, arguments); return PerformActionResult.Error($"Unrecognized action: {action}"); - } + } - void Traverse(Platform platform, IEnumerable elements, IList matches, Func matcher) + void Traverse(IEnumerable elements, IList matches, Func matcher) { foreach (var e in elements) { @@ -95,7 +79,7 @@ namespace Microsoft.Maui.Automation if (e.PlatformElement is View view) { var children = view.GetChildren(this, e.Id, 1, 1); - Traverse(platform, children, matches, matcher); + Traverse(children, matches, matcher); } } } diff --git a/Microsoft.Maui.Automation.AppHost/Platforms/Android/AndroidExtensions.cs b/Microsoft.Maui.Automation.AppHost/Platforms/Android/AndroidExtensions.cs index eed2d6b..dd28612 100644 --- a/Microsoft.Maui.Automation.AppHost/Platforms/Android/AndroidExtensions.cs +++ b/Microsoft.Maui.Automation.AppHost/Platforms/Android/AndroidExtensions.cs @@ -3,10 +3,12 @@ using Android.Content; using Android.Hardware.Lights; using Android.Views; using AndroidX.Core.View.Accessibility; +using Java.Lang; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Threading.Tasks; using static System.Net.Mime.MediaTypeNames; namespace Microsoft.Maui.Automation @@ -143,5 +145,46 @@ namespace Microsoft.Maui.Automation e.Children.AddRange(activity.GetChildren(e.Application, e.Id, currentDepth + 1, maxDepth)); return e; } + + + public static async Task PerformAction(this Android.Views.View view, string action, string elementId, params string[] arguments) + { + if (action == Actions.Tap) + { + await view.PostAsync(() => view.PerformClick()); + return PerformActionResult.Ok(); + } + + throw new NotSupportedException($"PerformAction {action} is not supported."); + } + + public static Task PostAsync(this Android.Views.View view, Action action) + { + var r = new AsyncThreadRunner(() => view.PerformClick()); + view.Post(r); + return r.WaitAsync(); + } + } + + class AsyncThreadRunner : Java.Lang.Object, IRunnable + { + public AsyncThreadRunner(Action action) + { + runner = action; + tcsRunner = new(); + } + + readonly Action runner; + + readonly TaskCompletionSource tcsRunner; + + public Task WaitAsync() + => tcsRunner.Task; + + public void Run() + { + runner?.Invoke(); + tcsRunner.TrySetResult(true); + } } } diff --git a/Microsoft.Maui.Automation.AppHost/Platforms/Windows/WindowsAppSdkApplication.cs b/Microsoft.Maui.Automation.AppHost/Platforms/Windows/WindowsAppSdkApplication.cs index b80e2fd..d8ba1e7 100644 --- a/Microsoft.Maui.Automation.AppHost/Platforms/Windows/WindowsAppSdkApplication.cs +++ b/Microsoft.Maui.Automation.AppHost/Platforms/Windows/WindowsAppSdkApplication.cs @@ -13,12 +13,12 @@ namespace Microsoft.Maui.Automation public override Platform DefaultPlatform => Platform.Winappsdk; - public override Task> GetElements(Platform platform) + public override Task> GetElements() => Task.FromResult>(new[] { UI.Xaml.Window.Current.GetElement(this, 1, -1) }); - public override async Task GetProperty(Platform platform, string elementId, string propertyName) + public override async Task GetProperty(string elementId, string propertyName) { - var matches = await FindElements(platform, e => e.Id?.Equals(elementId) ?? false); + var matches = await FindElements(e => e.Id?.Equals(elementId) ?? false); var match = matches?.FirstOrDefault(); @@ -29,23 +29,23 @@ namespace Microsoft.Maui.Automation } - public override async Task> FindElements(Platform platform, Func matcher) + public override async Task> FindElements(Func matcher) { var windows = new[] { UI.Xaml.Window.Current.GetElement(this, 1, 1) }; var matches = new List(); - await Traverse(platform, windows, matches, matcher); + await Traverse(windows, matches, matcher); return matches; } - public override Task PerformAction(Platform platform, string action, string elementId, params string[] arguments) + public override Task PerformAction(string action, string elementId, params string[] arguments) { return Task.FromResult(PerformActionResult.Error()); } - async Task Traverse(Platform platform, IEnumerable elements, IList matches, Func matcher) + async Task Traverse(IEnumerable elements, IList matches, Func matcher) { foreach (var e in elements) { @@ -62,7 +62,7 @@ namespace Microsoft.Maui.Automation children.Add(c); } - await Traverse(platform, children, matches, matcher); + await Traverse(children, matches, matcher); } } } diff --git a/Microsoft.Maui.Automation.Core/Application.cs b/Microsoft.Maui.Automation.Core/Application.cs index 3bc6f13..977f52a 100644 --- a/Microsoft.Maui.Automation.Core/Application.cs +++ b/Microsoft.Maui.Automation.Core/Application.cs @@ -56,17 +56,17 @@ namespace Microsoft.Maui.Automation public abstract Platform DefaultPlatform { get; } - public abstract Task GetProperty(Platform platform, string elementId, string propertyName); + public abstract Task GetProperty(string elementId, string propertyName); - public abstract Task> GetElements(Platform platform); + public abstract Task> GetElements(); - public abstract Task> FindElements(Platform platform, Func matcher); + public abstract Task> FindElements(Func matcher); - public Task PerformAction(Platform platform, string action, params string[] arguments) - => PerformAction(platform, action, string.Empty, arguments); + public Task PerformAction(string action, params string[] arguments) + => PerformAction(action, string.Empty, arguments); - public abstract Task PerformAction(Platform platform, string action, string elementId, params string[] arguments); + public abstract Task PerformAction(string action, string elementId, params string[] arguments); - } + } } diff --git a/Microsoft.Maui.Automation.Core/ElementExtensions.cs b/Microsoft.Maui.Automation.Core/ElementExtensions.cs index cb96a8a..4b64ef6 100644 --- a/Microsoft.Maui.Automation.Core/ElementExtensions.cs +++ b/Microsoft.Maui.Automation.Core/ElementExtensions.cs @@ -6,51 +6,40 @@ namespace Microsoft.Maui.Automation; public static class ElementExtensions { - public static Task FirstBy(this IApplication application, string propertyName, string pattern, bool isRegularExpression) - => application.FirstBy(application.DefaultPlatform, propertyName, pattern, isRegularExpression); - - public static Task FirstById(this IApplication application, string id) - => application.FirstById(application.DefaultPlatform, id); - - public static Task FirstByAutomationId(this IApplication application, string automationId) - => application.FirstByAutomationId(application.DefaultPlatform, automationId); - - - public static async Task FirstBy(this IApplication application, Platform platform, string propertyName, string pattern, bool isRegularExpression) - => (await application.FindElements(platform, e => e.Matches(propertyName, pattern, isRegularExpression))).FirstOrDefault(); - - public static async Task FirstById(this IApplication application, Platform platform, string id) - => (await application.FindElements(platform, e => e.Id.Equals(id))).FirstOrDefault(); - - public static async Task FirstByAutomationId(this IApplication application, Platform platform, string automationId) - => (await application.FindElements(platform, e => e.AutomationId.Equals(automationId))).FirstOrDefault(); - + public static async Task FirstBy(this IApplication application,string propertyName, string pattern, bool isRegularExpression) + => (await application.FindElements(e => e.PropertyMatches(propertyName, pattern, isRegularExpression))).FirstOrDefault(); + public static async Task FirstById(this IApplication application, string id) + => (await application.FindElements(e => e.Id.Equals(id))).FirstOrDefault(); + public static async Task FirstByAutomationId(this IApplication application, string automationId) + => (await application.FindElements(e => e.AutomationId.Equals(automationId))).FirstOrDefault(); + + + + public static Element? FirstBy(this IEnumerable elements, string propertyName, string pattern, bool isRegularExpression) + => elements.Traverse(e => e.PropertyMatches(propertyName, pattern, isRegularExpression)).FirstOrDefault(); + + public static Element? FirstById(this IEnumerable elements, string id) + => elements.Traverse(e => e.Id.Equals(id)).FirstOrDefault(); + + public static Element? FirstByAutomationId(this IEnumerable elements, string automationId) + => elements.Traverse(e => e.AutomationId.Equals(automationId)).FirstOrDefault(); + public static Task> By(this IApplication application, string propertyName, string pattern, bool isRegularExpression) - => application.By(application.DefaultPlatform, propertyName, pattern, isRegularExpression); + => application.FindElements(e => e.PropertyMatches(propertyName, pattern, isRegularExpression)); public static Task> ById(this IApplication application, string id) - => application.ById(application.DefaultPlatform, id); + => application.FindElements(e => e.Id.Equals(id)); public static Task> ByAutomationId(this IApplication application, string automationId) - => application.ByAutomationId(application.DefaultPlatform, automationId); - - - public static Task> By(this IApplication application, Platform platform, string propertyName, string pattern, bool isRegularExpression) - => application.FindElements(platform, e => e.Matches(propertyName, pattern, isRegularExpression)); - - public static Task> ById(this IApplication application, Platform platform, string id) - => application.FindElements(platform, e => e.Id.Equals(id)); - - public static Task> ByAutomationId(this IApplication application, Platform platform, string automationId) - => application.FindElements(platform, e => e.AutomationId.Equals(automationId)); + => application.FindElements(e => e.AutomationId.Equals(automationId)); public static IEnumerable By(this IEnumerable elements, string propertyName, string pattern, bool isRegularExpression) - => elements.Traverse(e => e.Matches(propertyName, pattern, isRegularExpression)); + => elements.Traverse(e => e.PropertyMatches(propertyName, pattern, isRegularExpression)); public static IEnumerable ById(this IEnumerable elements, string id) => elements.Traverse(e => e.Id.Equals(id)); @@ -59,30 +48,30 @@ public static class ElementExtensions => elements.Traverse(e => e.AutomationId.Equals(automationId)); - public static IEnumerable Find(this IEnumerable elements, Func matcher) - => elements.Traverse(matcher); + public static IEnumerable Find(this IEnumerable elements, Predicate predicate) + => elements.Traverse(predicate); - static IEnumerable Traverse(this IEnumerable elements, Func matcher) + static IEnumerable Traverse(this IEnumerable elements, Predicate predicate) { var matches = new List(); - elements.Traverse(matches, matcher); + elements.Traverse(matches, predicate); return matches; } - static void Traverse(this IEnumerable source, IList matches, Func matcher) + static void Traverse(this IEnumerable source, IList matches, Predicate predicate) { foreach (var s in source) { - if (matcher(s)) + if (predicate(s)) matches.Add(s); - Traverse(s.Children, matches, matcher); + Traverse(s.Children, matches, predicate); } } - internal static bool Matches(this Element e, string propertyName, string pattern, bool isRegularExpression = false) + public static bool PropertyMatches(this Element e, string propertyName, string pattern, bool isRegularExpression = false) { var value = propertyName.ToLowerInvariant() switch diff --git a/Microsoft.Maui.Automation.Core/GrpcRemoteAppAgent.cs b/Microsoft.Maui.Automation.Core/GrpcRemoteAppAgent.cs index 3a64490..821eb18 100644 --- a/Microsoft.Maui.Automation.Core/GrpcRemoteAppAgent.cs +++ b/Microsoft.Maui.Automation.Core/GrpcRemoteAppAgent.cs @@ -1,6 +1,7 @@ using Grpc.Core; using Grpc.Net.Client; using Grpc.Net.Client.Configuration; +using Grpc.Net.Client.Web; using Microsoft.Maui.Automation.RemoteGrpc; using System; using System.Collections.Generic; @@ -29,39 +30,11 @@ namespace Microsoft.Maui.Automation private bool disposedValue; - public GrpcRemoteAppAgent(IApplication application, string address, HttpMessageHandler? httpMessageHandler = null) + public GrpcRemoteAppAgent(IApplication application, string address) { Application = application; - var grpc = GrpcChannel.ForAddress(address, new GrpcChannelOptions - { - //ServiceConfig = new ServiceConfig - //{ - // MethodConfigs = - // { - // new MethodConfig - // { - // Names = { MethodName.Default }, - // RetryPolicy = new RetryPolicy - // { - // MaxAttempts = 60, - // InitialBackoff = TimeSpan.FromSeconds(1), - // MaxBackoff = TimeSpan.FromSeconds(5), - // BackoffMultiplier = 1.1, - // RetryableStatusCodes = { StatusCode.NotFound, StatusCode.Unavailable } - // } - // } - // } - //}, - //HttpHandler = new Grpc.Net.Client.Web.GrpcWebHandler(Grpc.Net.Client.Web.GrpcWebMode.GrpcWeb) - //, - //httpMessageHandler ?? new HttpClientHandler()) - //HttpHandler = new HttpClientHandler - //{ - // ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator - //} - }); ; - + var grpc = GrpcChannel.ForAddress(address); client = new RemoteApp.RemoteAppClient(grpc); elementsCall = client.GetElementsRoute(); @@ -87,7 +60,8 @@ namespace Microsoft.Maui.Automation var response = await HandleFindElementsRequest(findElementsCall.ResponseStream.Current); await findElementsCall.RequestStream.WriteAsync(response); } - } catch (Exception ex) + } + catch (Exception ex) { throw ex; } @@ -114,56 +88,104 @@ namespace Microsoft.Maui.Automation async Task 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); + + try + { + // Get the elements from the running app host + var elements = await GetApp(request.Platform).GetElements(); + response.Elements.AddRange(elements); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } return response; } async Task HandleFindElementsRequest(FindElementsRequest request) { - // Get the elements from the running app host - var elements = await Application.FindElements(request.Platform, e => - e.Matches(request.PropertyName, request.Pattern, request.IsExpression)); - var response = new ElementsResponse(); response.RequestId = request.RequestId; - response.Elements.AddRange(elements); + + try + { + // Get the elements from the running app host + var elements = await GetApp(request.Platform).FindElements(e => + e.PropertyMatches(request.PropertyName, request.Pattern, request.IsExpression)); + response.Elements.AddRange(elements); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } return response; } async Task HandleGetPropertyRequest(PropertyRequest request) { - // Get the elements from the running app host - var v = await Application.GetProperty(request.Platform, request.ElementId, request.PropertyName); - var response = new PropertyResponse(); response.Platform = request.Platform; response.RequestId = request.RequestId; - response.Value = v; + try + { + // Get the elements from the running app host + var v = await GetApp(request.Platform).GetProperty(request.ElementId, request.PropertyName); + + response.Value = v; + } + catch (Exception ex) + { + Console.WriteLine(ex); + response.Value = string.Empty; + } return response; + } async Task HandlePerformActionRequest(PerformActionRequest request) { - // Get the elements from the running app host - var result = await Application.PerformAction(request.Platform, request.Action, request.ElementId, request.Arguments.ToArray()); - var response = new PerformActionResponse(); response.Platform = request.Platform; response.RequestId = request.RequestId; - response.Status = result.Status; - response.Result = result.Result; + + try + { + // Get the elements from the running app host + var result = await GetApp(request.Platform).PerformAction(request.Action, request.ElementId, request.Arguments.ToArray()); + response.Status = result.Status; + response.Result = result.Result; + } + catch (Exception ex) + { + response.Status = PerformActionResult.ErrorStatus; + response.Result = ex.Message; + } return response; } + IApplication GetApp(Platform platform) + { + var unsupportedText = $"Platform {platform} is not supported on this app agent"; + + if (Application is MultiPlatformApplication multiApp) + { + if (!multiApp.PlatformApps.ContainsKey(platform)) + throw new NotSupportedException(unsupportedText); + + return multiApp.PlatformApps[platform]; + } + + if (Application.DefaultPlatform != platform) + throw new NotSupportedException(unsupportedText); + return Application; + } + protected virtual void Dispose(bool disposing) { if (!disposedValue) diff --git a/Microsoft.Maui.Automation.Core/IApplicationInterop.cs b/Microsoft.Maui.Automation.Core/IApplicationInterop.cs index 32bbf4d..e3ebd17 100644 --- a/Microsoft.Maui.Automation.Core/IApplicationInterop.cs +++ b/Microsoft.Maui.Automation.Core/IApplicationInterop.cs @@ -7,13 +7,14 @@ namespace Microsoft.Maui.Automation { public Platform DefaultPlatform { get; } - public Task> GetElements(Platform platform); + public Task> GetElements(); - public Task> FindElements(Platform platform, Func matcher); - - public Task GetProperty(Platform platform, string elementId, string propertyName); + public Task> FindElements(Func matcher); - public Task PerformAction(Platform platform, string action, string elementId, params string[] arguments); - public Task PerformAction(Platform platform, string action, params string[] arguments); + public Task GetProperty(string elementId, string propertyName); + + public Task PerformAction(string action, string elementId, params string[] arguments); + + public Task PerformAction(string action, params string[] arguments); } } \ No newline at end of file diff --git a/Microsoft.Maui.Automation.Core/MultiPlatformApplication.cs b/Microsoft.Maui.Automation.Core/MultiPlatformApplication.cs index 7312ef0..d001f4d 100644 --- a/Microsoft.Maui.Automation.Core/MultiPlatformApplication.cs +++ b/Microsoft.Maui.Automation.Core/MultiPlatformApplication.cs @@ -29,18 +29,18 @@ namespace Microsoft.Maui.Automation return PlatformApps[platform]; } - public override Task> GetElements(Platform platform) - => GetApp(platform).GetElements(platform); + public override Task> GetElements() + => GetApp(DefaultPlatform).GetElements(); - public override Task GetProperty(Platform platform, string elementId, string propertyName) - => GetApp(platform).GetProperty(platform, elementId, propertyName); + public override Task GetProperty(string elementId, string propertyName) + => GetApp(DefaultPlatform).GetProperty(elementId, propertyName); - public override Task> FindElements(Platform platform, Func matcher) - => GetApp(platform).FindElements(platform, matcher); + public override Task> FindElements(Func matcher) + => GetApp(DefaultPlatform).FindElements(matcher); - public override Task PerformAction(Platform platform, string action, string elementId, params string[] arguments) - => GetApp(platform).PerformAction(platform, action, elementId, arguments); + public override Task PerformAction(string action, string elementId, params string[] arguments) + => GetApp(DefaultPlatform).PerformAction(action, elementId, arguments); } } diff --git a/Microsoft.Maui.Automation.Core/PerformActionResult.cs b/Microsoft.Maui.Automation.Core/PerformActionResult.cs index 4d486e8..d35b9bf 100644 --- a/Microsoft.Maui.Automation.Core/PerformActionResult.cs +++ b/Microsoft.Maui.Automation.Core/PerformActionResult.cs @@ -5,15 +5,19 @@ public const string Tap = "TAP"; } - public class PerformActionResult + public partial class PerformActionResult { - public static PerformActionResult Ok(string result = "") - => new PerformActionResult { Result = result, Status = 0 }; + public const int SuccessStatus = 0; + public const int ErrorStatus = -1; - public static PerformActionResult Error(string result = "", int status = -1) + public static PerformActionResult Ok(string result = "") + => new PerformActionResult { Result = result, Status = SuccessStatus }; + + public static PerformActionResult Error(string result = "", int status = ErrorStatus) => new PerformActionResult { Result = result, Status = status }; - public int Status { get; set; } + public int Status { get; set; } = ErrorStatus; + public string Result { get; set; } = string.Empty; } } \ No newline at end of file diff --git a/Microsoft.Maui.Automation.Driver/AndroidDriver.cs b/Microsoft.Maui.Automation.Driver/AndroidDriver.cs index 6598898..660e37a 100644 --- a/Microsoft.Maui.Automation.Driver/AndroidDriver.cs +++ b/Microsoft.Maui.Automation.Driver/AndroidDriver.cs @@ -19,8 +19,8 @@ public class AndroidDriver : IDriver { Configuration = configuration; - int port = 10882; - var address = IPAddress.Any.ToString(); + int port = 5000; + //var address = IPAddress.Any.ToString(); var adbDeviceSerial = configuration.Device; androidSdkManager = new AndroidSdk.AndroidSdkManager(); @@ -40,8 +40,8 @@ public class AndroidDriver : IDriver Name = $"Android ({Adb.GetDeviceName(Device)})"; - //var forwardResult = Adb.RunCommand("-s", $"\"{Device}\"", "reverse", $"tcp:{port}", $"tcp:{port}")?.GetAllOutput(); - //System.Diagnostics.Debug.WriteLine(forwardResult); + var forwardResult = Adb.RunCommand("-s", $"\"{Device}\"", "reverse", $"tcp:{port}", $"tcp:{port}")?.GetAllOutput(); + System.Diagnostics.Debug.WriteLine(forwardResult); grpc = new GrpcHost(); } @@ -52,8 +52,9 @@ public class AndroidDriver : IDriver protected readonly AndroidSdk.PackageManager Pm; readonly AndroidSdk.AndroidSdkManager androidSdkManager; - protected readonly string Device; - + protected readonly string Device; + private bool disposedValue; + public IAutomationConfiguration Configuration { get; } public string Name { get; } @@ -177,19 +178,31 @@ public class AndroidDriver : IDriver } public Task Tap(int x, int y) - => grpc.Client.PerformAction(Platform.Android, Actions.Tap, string.Empty, x.ToString(), y.ToString()); + => grpc.Client.PerformAction(Configuration.AutomationPlatform, Actions.Tap, string.Empty, x.ToString(), y.ToString()); + + public Task Tap(Element element) + => grpc.Client.PerformAction(Configuration.AutomationPlatform, Actions.Tap, element.Id); + bool IsAppInstalled(string appId) => androidSdkManager.PackageManager.ListPackages() .Any(p => p.PackageName?.Equals(appId, StringComparison.OrdinalIgnoreCase) ?? false); - public Task GetProperty(Platform platform, string elementId, string propertyName) - => grpc.Client.GetProperty(platform, elementId, propertyName); + public Task GetProperty(string elementId, string propertyName) + => grpc.Client.GetProperty(Configuration.AutomationPlatform, elementId, propertyName); - public Task> GetElements(Platform platform) - => grpc.Client.GetElements(platform); + public Task> GetElements() + => grpc.Client.GetElements(Configuration.AutomationPlatform); - public Task> FindElements(Platform platform, string propertyName, string pattern, bool isExpression = false, string ancestorId = "") - => grpc.Client.FindElements(platform, propertyName, pattern, isExpression, ancestorId); + public Task> FindElements(string propertyName, string pattern, bool isExpression = false, string ancestorId = "") + => grpc.Client.FindElements(Configuration.AutomationPlatform, propertyName, pattern, isExpression, ancestorId); + public Task PerformAction(string action, string elementId, params string[] arguments) + => grpc.Client.PerformAction(Configuration.AutomationPlatform, action, elementId, arguments); + + public async void Dispose() + { + if (grpc is not null) + await grpc.Stop(); + } } diff --git a/Microsoft.Maui.Automation.Driver/AppDriver.cs b/Microsoft.Maui.Automation.Driver/AppDriver.cs index 7affb3a..eddf793 100644 --- a/Microsoft.Maui.Automation.Driver/AppDriver.cs +++ b/Microsoft.Maui.Automation.Driver/AppDriver.cs @@ -2,9 +2,10 @@ namespace Microsoft.Maui.Automation.Driver; -public class AppDriver : IDriver +public class AppDriver : Driver { public AppDriver(IAutomationConfiguration configuration) + : base(configuration) { Driver = configuration.DevicePlatform switch { @@ -19,61 +20,70 @@ public class AppDriver : IDriver public readonly IDriver Driver; - public IAutomationConfiguration Configuration => Driver!.Configuration; + public override string Name => Driver.Name; - public string Name => Driver.Name; - - public Task Back() + public override Task Back() => Driver.Back(); - public Task ClearAppState(string appId) + public override Task ClearAppState(string appId) => Driver.ClearAppState(appId); - public Task> FindElements(Platform platform, string propertyName, string pattern, bool isExpression = false, string ancestorId = "") - => Driver.FindElements(platform, propertyName, pattern, isExpression, propertyName); + public override Task> FindElements(string propertyName, string pattern, bool isExpression = false, string ancestorId = "") + => Driver.FindElements(propertyName, pattern, isExpression, propertyName); - public Task GetDeviceInfo() + public override Task GetDeviceInfo() => Driver.GetDeviceInfo(); - public Task> GetElements(Platform platform) - => Driver.GetElements(platform); + public override Task> GetElements() + => Driver.GetElements(); - public Task GetProperty(Platform platform, string elementId, string propertyName) - => Driver.GetProperty(platform, elementId, propertyName); + public override Task GetProperty(string elementId, string propertyName) + => Driver.GetProperty(elementId, propertyName); - public Task InputText(string text) + public override Task PerformAction(string action, string elementId, params string[] arguments) + => Driver.PerformAction(action, elementId, arguments); + + public override Task InputText(string text) => Driver.InputText(text); - public Task InstallApp(string file, string appId) + public override Task InstallApp(string file, string appId) => Driver.InstallApp(file, appId); - public Task KeyPress(char keyCode) + public override Task KeyPress(char keyCode) => Driver.KeyPress(keyCode); - public Task LaunchApp(string appId) + public override Task LaunchApp(string appId) => Driver.LaunchApp(appId); - public Task LongPress(int x, int y) + public override Task LongPress(int x, int y) => Driver.LongPress(x, y); - public Task OpenUri(string uri) + public override Task OpenUri(string uri) => Driver.OpenUri(uri); - public Task PullFile(string appId, string remoteFile, string localDirectory) + public override Task PullFile(string appId, string remoteFile, string localDirectory) => Driver.PullFile(appId, remoteFile, localDirectory); - public Task PushFile(string appId, string localFile, string destinationDirectory) + public override Task PushFile(string appId, string localFile, string destinationDirectory) => Driver.PushFile(appId, localFile, destinationDirectory); - public Task RemoveApp(string appId) + public override Task RemoveApp(string appId) => Driver.RemoveApp(appId); - public Task StopApp(string appId) + public override Task StopApp(string appId) => Driver.StopApp(appId); - public Task Swipe((int x, int y) start, (int x, int y) end) + public override Task Swipe((int x, int y) start, (int x, int y) end) => Driver.Swipe(start, end); - public Task Tap(int x, int y) + public override Task Tap(int x, int y) => Driver.Tap(x, y); + + public override Task Tap(Element element) + => Driver.PerformAction(Actions.Tap, element.Id); + + public override void Dispose() + { + Driver.Dispose(); + } } diff --git a/Microsoft.Maui.Automation.Driver/AutomationConfiguration.cs b/Microsoft.Maui.Automation.Driver/AutomationConfiguration.cs index ff40350..cde4115 100644 --- a/Microsoft.Maui.Automation.Driver/AutomationConfiguration.cs +++ b/Microsoft.Maui.Automation.Driver/AutomationConfiguration.cs @@ -42,8 +42,10 @@ namespace Microsoft.Maui.Automation.Driver { } - public AutomationConfiguration(Platform devicePlatform, string? device = null, Platform? automationPlatform = null) + public AutomationConfiguration(string appId, string appFilename, Platform devicePlatform, string? device = null, Platform? automationPlatform = null) { + AppId = appId; + AppFilename = appFilename; DevicePlatform = devicePlatform; if (!string.IsNullOrEmpty(device)) Device = device; @@ -58,7 +60,7 @@ namespace Microsoft.Maui.Automation.Driver public int AppAgentPort { - get => int.Parse(GetOrDefault(nameof(AppAgentPort), 10882).ToString()); + get => int.Parse(GetOrDefault(nameof(AppAgentPort), 5000).ToString()); set => this[nameof(AppAgentPort)] = value; } @@ -80,7 +82,19 @@ namespace Microsoft.Maui.Automation.Driver set => this[nameof(AutomationPlatform)] = Enum.GetName(value) ?? nameof(Platform.Maui); } - private object GetOrDefault(string key, object defaultValue) + public string AppId + { + get => GetOrDefault(nameof(AppId), "").ToString(); + set => this[nameof(AppId)] = value; + } + + public string AppFilename + { + get => GetOrDefault(nameof(AppFilename), "").ToString(); + set => this[nameof(AppFilename)] = value; + } + + private object GetOrDefault(string key, object defaultValue) { if (this.ContainsKey(key)) return this[key]; diff --git a/Microsoft.Maui.Automation.Driver/Driver.cs b/Microsoft.Maui.Automation.Driver/Driver.cs new file mode 100644 index 0000000..96e216a --- /dev/null +++ b/Microsoft.Maui.Automation.Driver/Driver.cs @@ -0,0 +1,77 @@ +namespace Microsoft.Maui.Automation.Driver +{ + public abstract class Driver : IDriver + { + public Driver(IAutomationConfiguration configuration) + { + Configuration = configuration; + } + + public abstract string Name { get; } + + public IAutomationConfiguration Configuration { get; } + + public abstract Task Back(); + + public abstract Task ClearAppState(string appId); + public Task ClearAppState() + => ClearAppState(Configuration.AppId); + + + public abstract void Dispose(); + + public abstract Task GetDeviceInfo(); + + public abstract Task InputText(string text); + + public abstract Task InstallApp(string file, string appId); + + public Task InstallApp() + => InstallApp(Configuration.AppFilename, Configuration.AppId); + + public abstract Task KeyPress(char keyCode); + + public abstract Task LaunchApp(string appId); + + public Task LaunchApp() + => LaunchApp(Configuration.AppId); + + public abstract Task LongPress(int x, int y); + + public abstract Task OpenUri(string uri); + + public abstract Task> FindElements(string propertyName, string pattern, bool isExpression = false, string ancestorId = ""); + + public abstract Task> GetElements(); + + public abstract Task GetProperty(string elementId, string propertyName); + + public abstract Task PerformAction(string action, string elementId, params string[] arguments); + + public abstract Task PullFile(string appId, string remoteFile, string localDirectory); + + public Task PullFile(string remoteFile, string localDirectory) + => PullFile(Configuration.AppId, remoteFile, localDirectory); + + public abstract Task PushFile(string appId, string localFile, string destinationDirectory); + + public Task PushFile(string localFile, string destinationDirectory) + => PushFile(Configuration.AppId, localFile, destinationDirectory); + + public abstract Task RemoveApp(string appId); + + public Task RemoveApp() + => RemoveApp(Configuration.AppId); + + public abstract Task StopApp(string appId); + + public Task StopApp() + => StopApp(Configuration.AppId); + + public abstract Task Swipe((int x, int y) start, (int x, int y) end); + + public abstract Task Tap(int x, int y); + + public abstract Task Tap(Element element); + } +} \ No newline at end of file diff --git a/Microsoft.Maui.Automation.Driver/DriverExtensions.cs b/Microsoft.Maui.Automation.Driver/DriverExtensions.cs new file mode 100644 index 0000000..d0c493e --- /dev/null +++ b/Microsoft.Maui.Automation.Driver/DriverExtensions.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Maui.Automation.Driver; + +public static class DriverExtensions +{ + + public static async Task FirstById(this IDriver driver, string id) + { + var elements = await driver.GetElements(); + + return elements.FirstById(id); + } + + public static async Task FirstByAutomationId(this IDriver driver, string automationId) + { + var elements = await driver.GetElements(); + + var e = elements.FirstByAutomationId(automationId); + return e; + } + + public static async Task> By(this IDriver driver, Predicate matching) + { + var elements = await driver.GetElements(); + + return elements.Find(matching); + } + + + + public static Task> ByType(this IDriver driver, string elementType) + => driver.FindElements("Type", elementType); +} diff --git a/Microsoft.Maui.Automation.Driver/GrpcHost.cs b/Microsoft.Maui.Automation.Driver/GrpcHost.cs index b19fccf..d4272a5 100644 --- a/Microsoft.Maui.Automation.Driver/GrpcHost.cs +++ b/Microsoft.Maui.Automation.Driver/GrpcHost.cs @@ -1,44 +1,68 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Microsoft.Maui.Automation.Remote; namespace Microsoft.Maui.Automation.Remote; public class GrpcHost { - public GrpcHost() - { - var builder = CreateHostBuilder(this, Array.Empty()); + public GrpcHost() + { + var builder = CreateHostBuilder(this, Array.Empty()); - host = builder.Build(); - host.StartAsync(); + host = builder.Build(); + host.StartAsync(); - Client = host.Services.GetRequiredService(); - } + Services = host.Services; + Client = host.Services.GetRequiredService(); + } - public readonly GrpcRemoteAppClient Client; + public readonly GrpcRemoteAppClient Client; - readonly IHost host; + readonly IWebHost host; - public static IHostBuilder CreateHostBuilder(GrpcHost grpcHost, string[] args) - => Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => webBuilder - .ConfigureServices(services => { - services.AddGrpc(); - - services.AddSingleton(grpcHost); - services.AddSingleton(); + public readonly IServiceProvider Services; - }) - .Configure(app => - app.UseRouting() - .UseGrpcWeb() - .UseEndpoints(endpoints => - endpoints.MapGrpcService().EnableGrpcWeb()))); + public ILogger Logger => Services.GetRequiredService>(); + public static IWebHostBuilder CreateHostBuilder(GrpcHost grpcHost, string[] args) + { + var builder = new WebHostBuilder() + .ConfigureLogging(log => + { + log.AddConsole(); + }) + .UseKestrel(kestrel => + { + kestrel.ListenAnyIP(5000, listen => + { + listen.Protocols = HttpProtocols.Http2; + }); + }) + .ConfigureServices(services => + { + services.AddGrpc(); + services.AddSingleton(grpcHost); + services.AddSingleton(); + }) + .Configure(app => + { + app + .UseRouting() + .UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true }) + .UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + }); + }); - public Task Stop() - => host?.StopAsync() ?? Task.CompletedTask; + return builder; + } + + public Task Stop() + => host?.StopAsync() ?? Task.CompletedTask; } diff --git a/Microsoft.Maui.Automation.Driver/GrpcRemoteAppClient.cs b/Microsoft.Maui.Automation.Driver/GrpcRemoteAppClient.cs index 110f699..4a32518 100644 --- a/Microsoft.Maui.Automation.Driver/GrpcRemoteAppClient.cs +++ b/Microsoft.Maui.Automation.Driver/GrpcRemoteAppClient.cs @@ -1,19 +1,15 @@ using Grpc.Core; +using Microsoft.Extensions.Logging; using Microsoft.Maui.Automation.RemoteGrpc; -using System; -using System.Collections.Concurrent; -using System.Linq.Expressions; -using System.Text.RegularExpressions; -using System.Threading.Tasks; namespace Microsoft.Maui.Automation.Remote { - public class GrpcRemoteAppClient : RemoteGrpc.RemoteApp.RemoteAppBase + public class GrpcRemoteAppClient : RemoteApp.RemoteAppBase { - public GrpcRemoteAppClient() + public GrpcRemoteAppClient(ILogger logger) : base() { - Console.WriteLine("New grpc Service"); + logger.LogInformation("GRPC Service Created"); } Dictionary> pendingResponses = new(); diff --git a/Microsoft.Maui.Automation.Driver/IAutomationConfiguration.cs b/Microsoft.Maui.Automation.Driver/IAutomationConfiguration.cs index e974cdf..c3b55e4 100644 --- a/Microsoft.Maui.Automation.Driver/IAutomationConfiguration.cs +++ b/Microsoft.Maui.Automation.Driver/IAutomationConfiguration.cs @@ -10,5 +10,8 @@ Platform DevicePlatform { get; set; } Platform AutomationPlatform { get; set; } + + string AppId { get; set; } + string AppFilename { get; set; } } } \ No newline at end of file diff --git a/Microsoft.Maui.Automation.Driver/IDriver.cs b/Microsoft.Maui.Automation.Driver/IDriver.cs index 419092e..040a606 100644 --- a/Microsoft.Maui.Automation.Driver/IDriver.cs +++ b/Microsoft.Maui.Automation.Driver/IDriver.cs @@ -1,6 +1,6 @@ namespace Microsoft.Maui.Automation.Driver { - public interface IDriver + public interface IDriver : IDisposable { string Name { get; } @@ -9,6 +9,7 @@ Task GetDeviceInfo(); Task InstallApp(string file, string appId); + Task RemoveApp(string appId); Task LaunchApp(string appId); @@ -17,12 +18,14 @@ Task ClearAppState(string appId); Task PushFile(string appId, string localFile, string destinationDirectory); - Task PullFile(string appId, string remoteFile, string localDirectory); + Task PullFile(string appId, string remoteFile, string localDirectory); - Task Tap(int x, int y); + Task Tap(int x, int y); Task LongPress(int x, int y); + Task Tap(Element element); + Task KeyPress(char keyCode); Task Swipe((int x, int y) start, (int x, int y) end); @@ -33,10 +36,12 @@ Task OpenUri(string uri); - Task GetProperty(Platform platform, string elementId, string propertyName); + Task GetProperty(string elementId, string propertyName); - Task> GetElements(Platform platform); + Task> GetElements(); - Task> FindElements(Platform platform, string propertyName, string pattern, bool isExpression = false, string ancestorId = ""); + Task> FindElements(string propertyName, string pattern, bool isExpression = false, string ancestorId = ""); + + Task PerformAction(string action, string elementId, params string[] arguments); } } \ No newline at end of file diff --git a/Microsoft.Maui.Automation.Driver/WindowsDriver.cs b/Microsoft.Maui.Automation.Driver/WindowsDriver.cs index d38bee9..d21cc08 100644 --- a/Microsoft.Maui.Automation.Driver/WindowsDriver.cs +++ b/Microsoft.Maui.Automation.Driver/WindowsDriver.cs @@ -6,104 +6,115 @@ using System.Net; namespace Microsoft.Maui.Automation.Driver { - public class WindowsDriver : IDriver - { + public class WindowsDriver : IDriver + { public WindowsDriver(IAutomationConfiguration configuration) { - Configuration = configuration; + Configuration = configuration; - grpc = new GrpcHost(); - } + grpc = new GrpcHost(); + } - readonly GrpcHost grpc; + readonly GrpcHost grpc; - public string Name => "Windows"; + public string Name => "Windows"; - public IAutomationConfiguration Configuration { get; } + public IAutomationConfiguration Configuration { get; } - public Task Back() - => Task.CompletedTask; + public Task Back() + => Task.CompletedTask; - public Task ClearAppState(string appId) - { - throw new NotImplementedException(); - } + public Task ClearAppState(string appId) + { + throw new NotImplementedException(); + } - public Task GetDeviceInfo() - { - throw new NotImplementedException(); - } + public Task GetDeviceInfo() + { + throw new NotImplementedException(); + } - public Task InputText(string text) - { - throw new NotImplementedException(); - } + public Task InputText(string text) + { + throw new NotImplementedException(); + } - public Task InstallApp(string file, string appId) - { - throw new NotImplementedException(); - } + public Task InstallApp(string file, string appId) + { + throw new NotImplementedException(); + } - public Task KeyPress(char keyCode) - { - throw new NotImplementedException(); - } + public Task KeyPress(char keyCode) + { + throw new NotImplementedException(); + } - public Task LaunchApp(string appId) - { - throw new NotImplementedException(); - } + public Task LaunchApp(string appId) + { + throw new NotImplementedException(); + } - public Task LongPress(int x, int y) - { - throw new NotImplementedException(); - } + public Task LongPress(int x, int y) + { + throw new NotImplementedException(); + } - public Task OpenUri(string uri) - { - throw new NotImplementedException(); - } + public Task OpenUri(string uri) + { + throw new NotImplementedException(); + } - public Task PullFile(string appId, string remoteFile, string localDirectory) - { - throw new NotImplementedException(); - } + public Task PullFile(string appId, string remoteFile, string localDirectory) + { + throw new NotImplementedException(); + } - public Task PushFile(string appId, string localFile, string destinationDirectory) - { - throw new NotImplementedException(); - } + public Task PushFile(string appId, string localFile, string destinationDirectory) + { + throw new NotImplementedException(); + } - public Task RemoveApp(string appId) - { - throw new NotImplementedException(); - } + public Task RemoveApp(string appId) + { + throw new NotImplementedException(); + } - public Task StopApp(string appId) - { - throw new NotImplementedException(); - } + public Task StopApp(string appId) + { + throw new NotImplementedException(); + } - public Task Swipe((int x, int y) start, (int x, int y) end) - { - throw new NotImplementedException(); - } + public Task Swipe((int x, int y) start, (int x, int y) end) + { + throw new NotImplementedException(); + } - public Task Tap(int x, int y) - { - throw new NotImplementedException(); - } + public Task Tap(int x, int y) + { + throw new NotImplementedException(); + } - public Task GetProperty(Platform platform, string elementId, string propertyName) - => grpc.Client.GetProperty(platform, elementId, propertyName); + public Task Tap(Element element) + => grpc.Client.PerformAction(Configuration.AutomationPlatform, Actions.Tap, element.Id); - public Task> GetElements(Platform platform) - => grpc.Client.GetElements(platform); + public Task GetProperty(string elementId, string propertyName) + => grpc.Client.GetProperty(Configuration.AutomationPlatform, elementId, propertyName); - public Task> FindElements(Platform platform, string propertyName, string pattern, bool isExpression = false, string ancestorId = "") - => grpc.Client.FindElements(platform, propertyName, pattern, isExpression, ancestorId); + public Task> GetElements() + => grpc.Client.GetElements(Configuration.AutomationPlatform); - } + public Task> FindElements(string propertyName, string pattern, bool isExpression = false, string ancestorId = "") + => grpc.Client.FindElements(Configuration.AutomationPlatform, propertyName, pattern, isExpression, ancestorId); + + public Task PerformAction(string action, string elementId, params string[] arguments) + => grpc.Client.PerformAction(Configuration.AutomationPlatform, action, elementId, arguments); + + public async void Dispose() + { + if (grpc is not null) + await grpc.Stop(); + } + } } diff --git a/Microsoft.Maui.Automation.Driver/iOSDriver.cs b/Microsoft.Maui.Automation.Driver/iOSDriver.cs index afea0ac..4f6bfab 100644 --- a/Microsoft.Maui.Automation.Driver/iOSDriver.cs +++ b/Microsoft.Maui.Automation.Driver/iOSDriver.cs @@ -187,7 +187,11 @@ public class iOSDriver : IDriver => idb.hid().SendStream(keyCode.AsHidEvents().ToArray()); public Task Tap(int x, int y) - => press(x, y, TimeSpan.FromMilliseconds(50)); + => press(x, y, TimeSpan.FromMilliseconds(50)); + + public Task Tap(Element element) + => grpc.Client.PerformAction(Platform.Ios, Actions.Tap, element.Id); + public Task LongPress(int x, int y) => press(x, y, TimeSpan.FromSeconds(3)); @@ -247,15 +251,17 @@ public class iOSDriver : IDriver } }); - public Task GetProperty(Platform platform, string elementId, string propertyName) - => grpc.Client.GetProperty(platform, elementId, propertyName); + public Task GetProperty(string elementId, string propertyName) + => grpc.Client.GetProperty(Configuration.AutomationPlatform, elementId, propertyName); - public Task> GetElements(Platform platform) - => grpc.Client.GetElements(platform); + public Task> GetElements() + => grpc.Client.GetElements(Configuration.AutomationPlatform); - public Task> FindElements(Platform platform, string propertyName, string pattern, bool isExpression = false, string ancestorId = "") - => grpc.Client.FindElements(platform, propertyName, pattern, isExpression, ancestorId); + public Task> FindElements(string propertyName, string pattern, bool isExpression = false, string ancestorId = "") + => grpc.Client.FindElements(Configuration.AutomationPlatform, propertyName, pattern, isExpression, ancestorId); + public Task PerformAction(string action, string elementId, params string[] arguments) + => grpc.Client.PerformAction(Configuration.AutomationPlatform, action, elementId, arguments); string UnpackIdb() { @@ -272,4 +278,10 @@ public class iOSDriver : IDriver return exePath; } + + public async void Dispose() + { + if (grpc is not null) + await grpc.Stop(); + } } diff --git a/Microsoft.Maui.Automation.Repl/Program.cs b/Microsoft.Maui.Automation.Repl/Program.cs index c7b1372..84ce818 100644 --- a/Microsoft.Maui.Automation.Repl/Program.cs +++ b/Microsoft.Maui.Automation.Repl/Program.cs @@ -1,5 +1,8 @@ -using Grpc.Net.Client; +using Grpc.Core.Logging; +using Grpc.Net.Client; +using Microsoft.Extensions.Logging; using Microsoft.Maui.Automation; +using Microsoft.Maui.Automation.Driver; using Microsoft.Maui.Automation.Remote; using Spectre.Console; using System.Net; @@ -8,52 +11,35 @@ using System.Net; var platform = Platform.Maui; -var grpc = new GrpcHost(); -Console.WriteLine("Started GRPC Host."); - - -var client = grpc.Client; - -while (true) +var config = new AutomationConfiguration { - var input = Console.ReadLine() ?? string.Empty; + AppAgentPort = 5000, + DevicePlatform = Platform.Android, + AutomationPlatform = platform, + Device = "emulator-5554" +}; +var driver = new AppDriver(config); - try - { - if (input.StartsWith("tree")) +var mappings = new Dictionary> +{ + { "tree", Tree }, + { "windows", Windows }, + { "test2", async () => { - var children = await client.GetElements(platform); + var button = await driver.FirstByAutomationId("buttonOne"); - foreach (var w in children) - { - var tree = new Tree(w.ToTable(ConfigureTable)); + await driver.Tap(button!); + var label = await driver.FirstByAutomationId("labelCount"); - foreach (var d in w.Children) - { - PrintTree(tree, d, 1); - } - - AnsiConsole.Write(tree); - } - + Console.WriteLine(label.Text); } - else if (input.StartsWith("windows")) + }, + { "test", async () => { - var children = await client.GetElements(platform); + var elements = await driver.FindElements("AutomationId", "buttonOne"); - foreach (var w in children) - { - var tree = new Tree(w.ToTable(ConfigureTable)); - - AnsiConsole.Write(tree); - } - } - else if (input.StartsWith("test")) - { - var elements = await client.FindElements(platform, "AutomationId", "buttonOne"); - foreach (var w in elements) { var tree = new Tree(w.ToTable(ConfigureTable)); @@ -62,9 +48,37 @@ while (true) } } } +}; + +while (true) +{ + var input = Console.ReadLine() ?? string.Empty; + + try + { + foreach (var kvp in mappings) + { + if (input.StartsWith(kvp.Key)) + { + Task.Run(async () => + { + try + { + await kvp.Value(); + } + catch (Exception ex) + { + AnsiConsole.WriteException(ex); + } + }); + + break; + } + } + } catch (Exception ex) { - Console.WriteLine(ex); + AnsiConsole.WriteException(ex); } if (input != null && (input.Equals("quit", StringComparison.OrdinalIgnoreCase) @@ -73,8 +87,37 @@ while (true) break; } -await grpc.Stop(); +driver.Dispose(); +async Task Tree() +{ + var children = await driver.GetElements(); + + foreach (var w in children) + { + var tree = new Tree(w.ToTable(ConfigureTable)); + + + foreach (var d in w.Children) + { + PrintTree(tree, d, 1); + } + + AnsiConsole.Write(tree); + } +} + +async Task Windows() +{ + var children = await driver.GetElements(); + + foreach (var w in children) + { + var tree = new Tree(w.ToTable(ConfigureTable)); + + AnsiConsole.Write(tree); + } +} void PrintTree(IHasTreeNodes node, Element element, int depth) { diff --git a/Microsoft.Maui.Automation.Test/Tests.cs b/Microsoft.Maui.Automation.Test/Tests.cs index f34e621..a9b7d30 100644 --- a/Microsoft.Maui.Automation.Test/Tests.cs +++ b/Microsoft.Maui.Automation.Test/Tests.cs @@ -10,37 +10,39 @@ namespace Microsoft.Maui.Automation.Test { public class Tests { + readonly AppDriver driver; + public Tests() { - - configuration = new AutomationConfiguration( - Platform.Android, - automationPlatform: Platform.Maui, - device: "emulator-5554"); - configuration.AppAgentPort = 5000; - - driver = new Driver.AppDriver(configuration); + driver = new AppDriver( + new AutomationConfiguration( + "com.companyname.samplemauiapp", + "C:\\code\\Maui.UITesting\\samples\\SampleMauiApp\\bin\\Debug\\net6.0-android\\com.companyname.samplemauiapp-Signed.apk", + Platform.Android, + automationPlatform: Platform.Maui, + device: "emulator-5554")); } - readonly IAutomationConfiguration configuration; - readonly AppDriver driver; - [Fact] public async Task RunApp() { - var appId = "com.companyname.samplemauiapp"; - var file = "C:\\code\\Maui.UITesting\\samples\\SampleMauiApp\\bin\\Debug\\net6.0-android\\com.companyname.samplemauiapp-Signed.apk"; + // Install and launch the app + await driver.InstallApp(); + await driver.LaunchApp(); + // Find the button by its MAUI AutomationId property + var button = await driver.FirstByAutomationId("buttonOne"); + Assert.NotNull(button); - //await driver.InstallApp(file, appId); + // Tap the button to increment the counter + await driver.Tap(button); - //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(); + // Find the label we expect to have changed + var label = await driver.By(e => + e.Type == "Label" + && e.Text.Contains("1")); + Assert.NotEmpty(label); } } } diff --git a/samples/SampleMauiApp/App.xaml.cs b/samples/SampleMauiApp/App.xaml.cs index 82af69e..6ae47da 100644 --- a/samples/SampleMauiApp/App.xaml.cs +++ b/samples/SampleMauiApp/App.xaml.cs @@ -12,7 +12,7 @@ namespace SampleMauiApp MainPage = new MainPage(); //#if ANDROID - this.StartAutomationServiceListener("http://127.0.0.1:5000"); + this.StartAutomationServiceListener("http://localhost:5000"); //#else //this.StartAutomationServiceListener("https://localhost:5001"); //#endif diff --git a/samples/SampleMauiApp/MainPage.xaml b/samples/SampleMauiApp/MainPage.xaml index 612cbf9..2af2df5 100644 --- a/samples/SampleMauiApp/MainPage.xaml +++ b/samples/SampleMauiApp/MainPage.xaml @@ -24,6 +24,7 @@