diff --git a/src/Core/Query/DescendantsQueryStep.cs b/src/Core/Query/DescendantsQueryStep.cs index fdffa61..0cb0b12 100644 --- a/src/Core/Query/DescendantsQueryStep.cs +++ b/src/Core/Query/DescendantsQueryStep.cs @@ -4,7 +4,8 @@ namespace Microsoft.Maui.Automation.Querying; public class DescendantsQueryStep : PredicateQueryStep { - public DescendantsQueryStep(Predicate? predicate) : base(predicate) + public DescendantsQueryStep(Predicate? predicate, string? predicateDescription = null) + : base(predicate, predicateDescription) { } @@ -13,6 +14,9 @@ public DescendantsQueryStep() : base() } public override Task> Execute(IDriver driver, IEnumerable tree, IEnumerable currentSet) - => Task.FromResult(currentSet.Traverse(Predicate)); + => Task.FromResult(currentSet.Traverse(Predicate)); + + public override string ToString() + => $"Descendants({base.ToString()})"; } diff --git a/src/Core/Query/DriverQuery.cs b/src/Core/Query/DriverQuery.cs index a25e352..4da590d 100644 --- a/src/Core/Query/DriverQuery.cs +++ b/src/Core/Query/DriverQuery.cs @@ -51,10 +51,29 @@ public async Task> Elements(int autoWaitMs = DefaultAutoWa public TaskAwaiter> GetAwaiter() => AutoWait().GetAwaiter(); + void LogQuery(IEnumerable elements, int waited) + { + var s = new StringBuilder(); + s.Append(Query.ToString()); + s.AppendLine($"\t ✅ (Waited {waited}ms)"); + Logger.LogInformation(s.ToString()); + } + + Exception LogQuery(Exception ex, int waited, IEnumerable elements) + { + var s = new StringBuilder(); + s.Append(Query.ToString()); + s.AppendLine($"\t ❌ (Waited {waited}ms)"); + Logger.LogInformation(s.ToString()); + Logger.LogError(ex, ex.Message); + + return ex; + } + async Task> AutoWait(int autoWaitMs = DefaultAutoWaitMilliseconds, int retryDelayMs = DefaultAutoWaitRetryMilliseconds, bool waitForNone = false) { - Logger.LogInformation($"[Query({Query.Id})] AutoWaiting..."); var waited = 0; + IEnumerable results = Enumerable.Empty(); while (waited < autoWaitMs || autoWaitMs <= 0) { @@ -63,7 +82,7 @@ async Task> AutoWait(int autoWaitMs = DefaultAutoWaitMilli var elements = await Driver.GetElements(platform).ConfigureAwait(false); - var results = await Query.Execute(Driver, elements); + results = await Query.Execute(Driver, elements); var anyResults = results.Any(); @@ -73,13 +92,9 @@ async Task> AutoWait(int autoWaitMs = DefaultAutoWaitMilli if (autoWaitMs <= 0 || !anyResults) { if (anyResults) - { - var ex = new ElementsStillFoundException(Query); - Logger.LogError(ex, $"[Query({Query.Id})] {ex.Message}"); - throw ex; - } + throw LogQuery(new ElementsStillFoundException(Query), waited, results); - Logger.LogInformation($"[Query({Query.Id})] Completed with {results.Count()} element(s)."); + LogQuery(results, waited); return results; } } @@ -88,28 +103,19 @@ async Task> AutoWait(int autoWaitMs = DefaultAutoWaitMilli // Wait until we find 1 or more if (autoWaitMs <= 0 || anyResults) { - Logger.LogInformation($"[Query({Query.Id})] Completed with {results.Count()} element(s)."); + LogQuery(results, waited); return results; } } - Logger.LogInformation($"[Query({Query.Id})] Waited {waited}ms, Waiting another {retryDelayMs}ms..."); - Thread.Sleep(retryDelayMs); + await Task.Delay(retryDelayMs); waited += retryDelayMs; } if (waitForNone) - { - var ex = new ElementsStillFoundException(Query); - Logger.LogError(ex, $"[Query({Query.Id})] {ex.Message}"); - throw ex; - } + throw LogQuery(new ElementsStillFoundException(Query), waited, results); else - { - var ex = new ElementsNotFoundException(Query); - Logger.LogError(ex, $"[Query({Query.Id})] {ex.Message}"); - throw ex; - } + throw LogQuery(new ElementsNotFoundException(Query), waited, results); } } diff --git a/src/Core/Query/FirstQueryStep.cs b/src/Core/Query/FirstQueryStep.cs index 8284436..d1095e2 100644 --- a/src/Core/Query/FirstQueryStep.cs +++ b/src/Core/Query/FirstQueryStep.cs @@ -7,7 +7,8 @@ class FirstQueryStep : PredicateQueryStep public FirstQueryStep() : base() { } - public FirstQueryStep(Predicate? predicate = null) : base(predicate) + public FirstQueryStep(Predicate? predicate = null, string? predicateDescription = null) + : base(predicate, predicateDescription) { } public override Task> Execute(IDriver driver, IEnumerable tree, IEnumerable currentSet) @@ -18,6 +19,9 @@ public override Task> Execute(IDriver driver, IEnumerable< return Task.FromResult>(new[] { first }); else return Task.FromResult(Enumerable.Empty()); - } + } + + public override string ToString() + => $"First({base.ToString()})"; } diff --git a/src/Core/Query/IndexQueryStep.cs b/src/Core/Query/IndexQueryStep.cs index 202444a..16c1192 100644 --- a/src/Core/Query/IndexQueryStep.cs +++ b/src/Core/Query/IndexQueryStep.cs @@ -2,7 +2,7 @@ namespace Microsoft.Maui.Automation.Querying; -public class IndexQueryStep : PredicateQueryStep +public class IndexQueryStep : QueryStep { public IndexQueryStep(int index) : base() @@ -10,12 +10,6 @@ public IndexQueryStep(int index) Index = index; } - public IndexQueryStep(int index, Predicate predicate) - : base(predicate) - { - Index = index; - } - public readonly int Index; public override Task> Execute(IDriver driver, IEnumerable tree, IEnumerable currentSet) @@ -29,6 +23,9 @@ public override Task> Execute(IDriver driver, IEnumerable< catch { } return Task.FromResult>(newSet); - } + } + + public override string ToString() + => $"Index({Index})"; } diff --git a/src/Core/Query/InteractionQueryStep.cs b/src/Core/Query/InteractionQueryStep.cs index 4aefa05..26a09d4 100644 --- a/src/Core/Query/InteractionQueryStep.cs +++ b/src/Core/Query/InteractionQueryStep.cs @@ -4,15 +4,17 @@ namespace Microsoft.Maui.Automation.Querying; public class InteractionQueryStep : IQueryStep { - public InteractionQueryStep(Func interaction) + public InteractionQueryStep(Func interaction, string? interactionDescription) { Interaction = interaction; + InteractionDescription = interactionDescription ?? "Custom"; } public virtual TimeSpan DefaultPauseBeforeInteraction => TimeSpan.FromMilliseconds(300); public virtual TimeSpan DefaultPauseAfterInteraction => TimeSpan.FromMilliseconds(300); public readonly Func Interaction; + public readonly string InteractionDescription; public async Task> Execute(IDriver driver, IEnumerable tree, IEnumerable currentSet) { @@ -27,7 +29,10 @@ public async Task> Execute(IDriver driver, IEnumerable $"{InteractionDescription}"; } diff --git a/src/Core/Query/PredicateQueryStep.cs b/src/Core/Query/PredicateQueryStep.cs index 3c50162..0a1ef89 100644 --- a/src/Core/Query/PredicateQueryStep.cs +++ b/src/Core/Query/PredicateQueryStep.cs @@ -4,14 +4,19 @@ namespace Microsoft.Maui.Automation.Querying; public class PredicateQueryStep : QueryStep { - public PredicateQueryStep(Predicate? predicate = null) + public PredicateQueryStep(Predicate? predicate = null, string? predicateDescription = null) : base() { - Predicate = predicate ?? new Predicate(e => true); + Predicate = predicate ?? new Predicate(e => true); + PredicateDescription = predicate is null ? string.Empty : predicateDescription ?? "Expression"; } - public readonly Predicate Predicate; - + public readonly Predicate Predicate; + public virtual string PredicateDescription { get; private set; } + public override Task> Execute(IDriver driver, IEnumerable tree, IEnumerable currentSet) - => Task.FromResult(currentSet.Where(e => Predicate.Invoke(e))); + => Task.FromResult(currentSet.Where(e => Predicate.Invoke(e))); + + public override string ToString() + => PredicateDescription; } diff --git a/src/Core/Query/Query.cs b/src/Core/Query/Query.cs index 26ea1d2..bcc0706 100644 --- a/src/Core/Query/Query.cs +++ b/src/Core/Query/Query.cs @@ -1,7 +1,9 @@ -using Microsoft.Extensions.Logging; +using System.Text; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Maui.Automation.Driver; - +using Microsoft.Maui.Automation.Driver; +using static System.Net.Mime.MediaTypeNames; + namespace Microsoft.Maui.Automation.Querying; public class Query @@ -16,41 +18,45 @@ public static class ConfigurationKeys public Query() { - Id = Guid.NewGuid().ToString(); + QueryId = Guid.NewGuid().ToString(); } public Query(Platform automationPlatform) { AutomationPlatform = automationPlatform; - Id = Guid.NewGuid().ToString(); + QueryId = Guid.NewGuid().ToString(); } - public readonly string Id; + public readonly string QueryId; List steps = new(); public static Query On(Platform automationPlatform) => new Query(automationPlatform); - public static Query By(Predicate predicate) - => new Query().Append(predicate); - - public static Query ByAutomationId(string automationId) - => By(e => e.AutomationId == automationId); + static Query by(Predicate predicate, string? predicateDescription = null) + => new Query().append(predicate, predicateDescription); + + public static Query By(Predicate predicate) + => by(predicate, null); + + public static Query AutomationId(string automationId) + => by(e => e.AutomationId == automationId, $"AutomationId='{automationId}'"); public static Query ById(string id) - => By(e => e.Id == id); + => by(e => e.Id == id, $"Id='{id}'"); - public static Query OfType(string type) - => By(e => e.Type == type); + public static Query Type(string type) + => by(e => e.Type == type, $"Type='{type}'"); public static Query ContainingText(string text, StringComparison comparisonType = StringComparison.InvariantCultureIgnoreCase) - => By(e => e.Text.Contains(text, comparisonType)); + => by(e => e.Text.Contains(text, comparisonType), $"$Text.Contains('{text}')"); public static Query Marked(string marked, StringComparison comparisonType = StringComparison.InvariantCultureIgnoreCase) - => By(e => e.Id.Equals(marked, comparisonType) + => by(e => e.Id.Equals(marked, comparisonType) || e.AutomationId.Equals(marked, comparisonType) - || e.Text.Equals(marked, comparisonType)); + || e.Text.Equals(marked, comparisonType), + $"Id='{marked}' OR AutomationId='{marked}' OR Text='{marked}'"); public Query Append(IQueryStep step) { @@ -59,11 +65,14 @@ public Query Append(IQueryStep step) } public Query Append(Predicate predicate) - { - steps.Add(new PredicateQueryStep(predicate)); - return this; - } - + => append(predicate, null); + + internal Query append(Predicate predicate, string? predicateDescription = null) + { + steps.Add(new PredicateQueryStep(predicate, predicateDescription)); + return this; + } + public Platform? AutomationPlatform { get; private set; } public async Task> Execute(IDriver driver, IEnumerable source) @@ -94,6 +103,25 @@ public async Task> Execute(IDriver driver, IEnumerable query.Append(new IndexQueryStep(index)); public static Query Tap(this Query query) - => query.Append(new InteractionQueryStep((driver, element) => driver.Tap(element))); + => query.Append(new InteractionQueryStep((driver, element) => driver.Tap(element), "Tap")); public static Query LongPress(this Query query) - => query.Append(new InteractionQueryStep((driver, element) => driver.LongPress(element))); + => query.Append(new InteractionQueryStep((driver, element) => driver.LongPress(element), "LongPress")); public static Query InputText(this Query query, string text) - => query.Append(new InteractionQueryStep((driver, element) => driver.InputText(element, text))); + => query.Append(new InteractionQueryStep((driver, element) => driver.InputText(element, text), $"InputText('{text}')")); public static Query ClearText(this Query query, string text) - => query.Append(new InteractionQueryStep((driver, element) => driver.ClearText(element))); + => query.Append(new InteractionQueryStep((driver, element) => driver.ClearText(element), "ClearText()")); } public static class DriverQueryExtensions { - static DriverQuery AppendChildren(this DriverQuery query, Predicate predicate) + static DriverQuery AppendChildren(this DriverQuery query, Predicate predicate, string? predicateDescription) { - query.Query.Append(predicate); + query.Query.append(predicate, predicateDescription); return query; } @@ -62,27 +62,30 @@ static DriverQuery Append(this DriverQuery query, IQueryStep step) } - public static DriverQuery By(this DriverQuery query, Predicate? predicate = null) - => query.Append(new DescendantsQueryStep(predicate)); + static DriverQuery by(this DriverQuery query, Predicate? predicate = null, string? predicateDescription = null) + => query.Append(new DescendantsQueryStep(predicate, predicateDescription)); + + public static DriverQuery By(this DriverQuery query, Predicate? predicate = null) + => by(query, predicate, "Predicate"); public static DriverQuery AutomationId(this DriverQuery query, string automationId) - => query.By(e => e.AutomationId == automationId); + => query.by(e => e.AutomationId == automationId, $"AutomationId='{automationId}'"); public static DriverQuery Id(this DriverQuery query, string id) - => query.By(e => e.Id == id); + => query.by(e => e.Id == id, $"Id='{id}'"); public static DriverQuery Type(this DriverQuery query, string typeName) - => query.By(e => e.Type == typeName); + => query.by(e => e.Type == typeName, $"Type='{typeName}'"); public static DriverQuery FullType(this DriverQuery query, string fullTypeName) - => query.By(e => e.FullType == fullTypeName); + => query.by(e => e.FullType == fullTypeName, $"FullType='{fullTypeName}'"); public static DriverQuery Text(this DriverQuery query, string text, StringComparison comparisonType = StringComparison.InvariantCultureIgnoreCase) - => query.By(e => e.Text.Equals(text, comparisonType)); + => query.by(e => e.Text.Equals(text, comparisonType), $"Text='{text}'"); public static DriverQuery ContainsText(this DriverQuery query, string text, StringComparison comparisonType = StringComparison.InvariantCultureIgnoreCase) - => query.By(e => e.Text.Contains(text, comparisonType)); + => query.by(e => e.Text.Contains(text, comparisonType), $"$Text.Contains('{text}')"); public static DriverQuery Marked(this DriverQuery query, string marked, StringComparison comparisonType = StringComparison.InvariantCultureIgnoreCase) => query.By(e => e.Id.Equals(marked, comparisonType) @@ -94,27 +97,28 @@ public static DriverQuery First(this DriverQuery query) public static DriverQuery Children(this DriverQuery query, Predicate predicate) - => query.AppendChildren(predicate); + => query.AppendChildren(predicate, "Predicate()"); public static DriverQuery ChildrenByAutomationId(this DriverQuery query, string automationId) - => query.AppendChildren(e => e.AutomationId == automationId); + => query.AppendChildren(e => e.AutomationId == automationId, $"AutomationId='{automationId}'"); public static DriverQuery ChildrenById(this DriverQuery query, string id) - => query.AppendChildren(e => e.Id == id); + => query.AppendChildren(e => e.Id == id, $"Id='{id}'"); public static DriverQuery ChildrenOfType(this DriverQuery query, string typeName) - => query.AppendChildren(e => e.Type == typeName); + => query.AppendChildren(e => e.Type == typeName, $"Type='{Type}'"); public static DriverQuery ChildrenOfFullType(this DriverQuery query, string fullTypeName) - => query.AppendChildren(e => e.FullType == fullTypeName); + => query.AppendChildren(e => e.FullType == fullTypeName, $"FullType='{fullTypeName}'"); public static DriverQuery ChildrenContainingText(this DriverQuery query, string text, StringComparison comparisonType = StringComparison.InvariantCultureIgnoreCase) - => query.AppendChildren(e => e.Text.Contains(text, comparisonType)); + => query.AppendChildren(e => e.Text.Contains(text, comparisonType), $"$Text.Contains('{text}')"); public static DriverQuery ChildrenMarked(this DriverQuery query, string marked, StringComparison comparisonType = StringComparison.InvariantCultureIgnoreCase) => query.AppendChildren(e => e.Id.Equals(marked, comparisonType) || e.AutomationId.Equals(marked, comparisonType) - || e.Text.Equals(marked, comparisonType)); + || e.Text.Equals(marked, comparisonType), + $"Id='{marked}' OR AutomationId='{marked}' OR Text='{marked}'"); public static DriverQuery Siblings(this DriverQuery query, Predicate? predicate = null) => query.Append(new SiblingsQueryStep(predicate)); @@ -123,16 +127,16 @@ public static DriverQuery Index(this DriverQuery query, int index) => query.Append(new IndexQueryStep(index)); public static DriverQuery Tap(this DriverQuery query) - => query.Append(new InteractionQueryStep((driver, element) => driver.Tap(element))); + => query.Append(new InteractionQueryStep((driver, element) => driver.Tap(element), "Tap()")); public static DriverQuery LongPress(this DriverQuery query) - => query.Append(new InteractionQueryStep((driver, element) => driver.LongPress(element))); + => query.Append(new InteractionQueryStep((driver, element) => driver.LongPress(element), "LongPress()")); public static DriverQuery InputText(this DriverQuery query, string text) - => query.Append(new InteractionQueryStep((driver, element) => driver.InputText(element, text))); + => query.Append(new InteractionQueryStep((driver, element) => driver.InputText(element, text), $"InputText('{text}')")); public static DriverQuery ClearText(this DriverQuery query) - => query.Append(new InteractionQueryStep((driver, element) => driver.ClearText(element))); + => query.Append(new InteractionQueryStep((driver, element) => driver.ClearText(element), "ClearText()")); } public static class On diff --git a/src/Core/Query/SiblingsQueryStep.cs b/src/Core/Query/SiblingsQueryStep.cs index c9b4bfc..05a0c41 100644 --- a/src/Core/Query/SiblingsQueryStep.cs +++ b/src/Core/Query/SiblingsQueryStep.cs @@ -7,7 +7,8 @@ class SiblingsQueryStep : PredicateQueryStep public SiblingsQueryStep() : base() { } - public SiblingsQueryStep(Predicate? predicate = null) : base(predicate) + public SiblingsQueryStep(Predicate? predicate = null, string? predicateDescription = null) + : base(predicate, predicateDescription) { } public override Task> Execute(IDriver driver, IEnumerable tree, IEnumerable currentSet) @@ -26,5 +27,8 @@ public override Task> Execute(IDriver driver, IEnumerable< } return Task.FromResult>(newSet); - } + } + + public override string ToString() + => $"[Siblings({base.ToString()})]"; } diff --git a/src/Repl/Program.cs b/src/Repl/Program.cs index 043b30a..338a498 100644 --- a/src/Repl/Program.cs +++ b/src/Repl/Program.cs @@ -27,7 +27,7 @@ .ConfigureDriver(c => c.Set(ConfigurationKeys.GrpcHostLoggingEnabled, true)); var driver = builder!.Build(); -var logger = builder!.Host!.Services!.GetRequiredService>(); +var logger = builder!.Host!.Services!.GetRequiredService>(); Task? readTask = null; @@ -65,21 +65,18 @@ await driver .First() .Tap(); - logger.LogInformation("Looking for OK"); await driver .Query(Platform.Ios) .Marked("OK", StringComparison.OrdinalIgnoreCase) .Tap(); // Fill in username/password - logger.LogInformation("Looking for ENTRY"); await driver .AutomationId("entryPassword") .First() .ClearText() .InputText("1234"); - logger.LogInformation("Looking for BUTTON"); // Click Login await driver .Type("Button")