diff --git a/Lombiq.Tests.UI.Samples/Tests/MonkeyTests.cs b/Lombiq.Tests.UI.Samples/Tests/MonkeyTests.cs index 896663c65..b8b7738ae 100644 --- a/Lombiq.Tests.UI.Samples/Tests/MonkeyTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/MonkeyTests.cs @@ -58,8 +58,17 @@ public Task TestCurrentPageAsMonkeyRecursivelyShouldWorkWithAnonymousUser() => public Task TestAdminPagesAsMonkeyRecursivelyShouldWorkWithAdminUser() => ExecuteTestAfterSetupAsync( context => + { // Monkey tests needn't all start from the homepage. This one starts from the Orchard admin dashboard. - context.TestAdminAsMonkeyRecursivelyAsync(CreateMonkeyTestingOptions()), + + var monkeyTestingOptions = CreateMonkeyTestingOptions(); + + // So we don't take too much time testing the whole Orchard admin, this sample restricts requests to + // "/Admin". But this is just this sample, you can unleash monkeys on the whole admin too! + monkeyTestingOptions.UrlFilters.Add(new MatchesRegexMonkeyTestingUrlFilter("/Admin$")); + + return context.TestAdminAsMonkeyRecursivelyAsync(monkeyTestingOptions); + }, configuration => configuration.AssertBrowserLog = logEntries => logEntries.ShouldNotContain( logEntry => IsValidAdminBrowserLogEntry(logEntry), diff --git a/Lombiq.Tests.UI/Extensions/ExtendedLoggingExtensions.cs b/Lombiq.Tests.UI/Extensions/ExtendedLoggingExtensions.cs index 849fb0b32..32bfe12f2 100644 --- a/Lombiq.Tests.UI/Extensions/ExtendedLoggingExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/ExtendedLoggingExtensions.cs @@ -105,6 +105,7 @@ private static void ExecuteSection(this UITestContext context, LogSection sectio } catch (StaleElementReferenceException) when (notLast) { + LogStaleElementReferenceExceptionRetry(context, i); Thread.Sleep(TimeSpan.FromSeconds(1)); } } @@ -122,6 +123,7 @@ private static TResult ExecuteSection(this UITestContext context, LogSe } catch (StaleElementReferenceException) when (notLast) { + LogStaleElementReferenceExceptionRetry(context, i); Thread.Sleep(TimeSpan.FromSeconds(1)); } } @@ -150,16 +152,26 @@ private static async Task ExecuteSectionAsync( // multiple sections are started in concurrent threads, the result will be incorrect. This shouldn't be too much // of an issue for now though since tests, while async, are single-threaded. context.Scope.AtataContext.Log.Start(section); + context.Scope.AtataContext.Log.Info("Log section {0} started.", section.Message); var result = await functionAsync(); + context.Scope.AtataContext.Log.Info("Log section {0} ended.", section.Message); context.Scope.AtataContext.Log.EndSection(); return result; } catch (StaleElementReferenceException) when (notLast) { + LogStaleElementReferenceExceptionRetry(context, i); await Task.Delay(TimeSpan.FromSeconds(1)); } } throw new InvalidOperationException("Impossible to reach."); } + + private static void LogStaleElementReferenceExceptionRetry(UITestContext context, int tryIndex) => + context.Scope.AtataContext.Log.Info( + "The operation in the log section failed with StaleElementReferenceException but will be retried. This " + + "is try number {0} out of {1}.", + tryIndex + 1, + StabilityRetryCount); } diff --git a/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs index 99b15cda7..01acae2a2 100644 --- a/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/NavigationUITestContextExtensions.cs @@ -284,6 +284,14 @@ private static async Task SetFieldDropdownByIndexAsync(UITestContext context, By public static Task ClickReliablyOnAsync(this UITestContext context, By by, int maxTries = 3) => context.Get(by).ClickReliablyAsync(context, maxTries); + /// + /// Reliably clicks on the link identified by the given text with . + /// + /// The maximum number of clicks attempted altogether, if retries are needed. + public static Task ClickReliablyOnByLinkTextAsync(this UITestContext context, string linkText, int maxTries = 3) => + context.Get(By.LinkText(linkText)).ClickReliablyAsync(context, maxTries); + /// /// A convenience method that merges and and so the doesn't have to be passed twice. + /// + public static Task ClickReliablyOnUntilUrlChangeAsync( + this UITestContext context, + By by, + TimeSpan? timeout = null, + TimeSpan? interval = null) => + context.Get(by).ClickReliablyUntilUrlChangeAsync(context, timeout, interval); + /// /// Switches control to JS alert box, accepts it, and switches control back to main document or first frame. /// diff --git a/Lombiq.Tests.UI/Extensions/NavigationWebElementExtensions.cs b/Lombiq.Tests.UI/Extensions/NavigationWebElementExtensions.cs index 9a8fc7381..6ec54ff52 100644 --- a/Lombiq.Tests.UI/Extensions/NavigationWebElementExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/NavigationWebElementExtensions.cs @@ -66,8 +66,11 @@ await context.Configuration.Events.AfterClick }); /// - /// Repeatedly clicks an element until the browser leaves the page. If you're doing a Get() before then use instead. + /// Repeatedly clicks an element until the browser leaves the page. Note that unlike this doesn't just necessitate a URL change but also a page leave. If + /// you're doing a Get() before then use instead. /// public static Task ClickReliablyUntilPageLeaveAsync( this IWebElement element, @@ -82,4 +85,29 @@ public static Task ClickReliablyUntilPageLeaveAsync( }, timeout, interval); + + /// + /// Repeatedly clicks an element until the browser URL changes. Note that unlike this doesn't necessitate a page leave, but can include it. If you're + /// doing a Get() before then use instead. + /// + public static Task ClickReliablyUntilUrlChangeAsync( + this IWebElement element, + UITestContext context, + TimeSpan? timeout = null, + TimeSpan? interval = null) + { + var originalUri = context.GetCurrentUri(); + + return context.DoWithRetriesOrFailAsync( + async () => + { + await element.ClickReliablyAsync(context); + return context.GetCurrentUri() != originalUri; + }, + timeout, + interval); + } } diff --git a/Lombiq.Tests.UI/Extensions/OrchardCoreDashboardUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/OrchardCoreDashboardUITestContextExtensions.cs index 64bb92d62..c6c33bd25 100644 --- a/Lombiq.Tests.UI/Extensions/OrchardCoreDashboardUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/OrchardCoreDashboardUITestContextExtensions.cs @@ -98,11 +98,11 @@ public static async Task ClickNewContentItemAsync(this UITestContext context, st if (dropdown) { await context.ClickReliablyOnAsync(By.Id("new-dropdown")); - await context.ClickReliablyOnAsync(By.LinkText(contentItemName)); + await context.ClickReliablyOnByLinkTextAsync(contentItemName); } else { - await context.ClickReliablyOnAsync(By.LinkText($"New {contentItemName}")); + await context.ClickReliablyOnByLinkTextAsync($"New {contentItemName}"); } } diff --git a/Lombiq.Tests.UI/Extensions/TenantsUITestContextExtensions.cs b/Lombiq.Tests.UI/Extensions/TenantsUITestContextExtensions.cs index 5872a2846..263539694 100644 --- a/Lombiq.Tests.UI/Extensions/TenantsUITestContextExtensions.cs +++ b/Lombiq.Tests.UI/Extensions/TenantsUITestContextExtensions.cs @@ -16,7 +16,7 @@ public static async Task CreateAndSwitchToTenantManuallyAsync( { await context.CreateTenantManuallyAsync(name, urlPrefix, urlHost, featureProfile, navigate); - await context.ClickReliablyOnAsync(By.LinkText("Setup")); + await context.ClickReliablyOnByLinkTextAsync("Setup"); context.SwitchCurrentTenant(name, urlPrefix); } @@ -34,7 +34,7 @@ public static async Task CreateTenantManuallyAsync( await context.GoToAdminRelativeUrlAsync("/Tenants"); } - await context.ClickReliablyOnAsync(By.LinkText("Add Tenant")); + await context.ClickReliablyOnByLinkTextAsync("Add Tenant"); await context.ClickAndFillInWithRetriesAsync(By.Id("Name"), name); if (!string.IsNullOrEmpty(urlPrefix))