diff --git a/Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests.cs b/Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests.cs index 2ff9f1ffc..15062985f 100644 --- a/Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests.cs +++ b/Lombiq.Tests.UI.Samples/Tests/BasicVisualVerificationTests.cs @@ -54,7 +54,7 @@ public Task VerifyBlogImage(Browser browser) => // is the different rendering of text on each platform, but it can occur between different Linux distributions too. // Here: https://pandasauce.org/post/linux-fonts/ you can find a good summary about this from 2019, but still valid // in 2022. - [Theory, Chrome] + [Theory, Chrome, Edge] public Task VerifyNavbar(Browser browser) => ExecuteTestAfterSetupAsync( context => diff --git a/Lombiq.Tests.UI/Services/AtataFactory.cs b/Lombiq.Tests.UI/Services/AtataFactory.cs index 854fd6e92..a94f8bc70 100644 --- a/Lombiq.Tests.UI/Services/AtataFactory.cs +++ b/Lombiq.Tests.UI/Services/AtataFactory.cs @@ -3,7 +3,9 @@ using Lombiq.HelpfulLibraries.Common.Utilities; using OpenQA.Selenium; using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +using System.Threading.Tasks; using Xunit.Abstractions; namespace Lombiq.Tests.UI.Services; @@ -16,7 +18,16 @@ public class AtataConfiguration public static class AtataFactory { + [Obsolete($"Please use {nameof(StartAtataScopeAsync)} instead.")] + [SuppressMessage("Usage", "VSTHRD110:Observe result of async calls", Justification = "Backwards compatibility.")] + [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "Backwards compatibility.")] public static AtataScope StartAtataScope( + ITestOutputHelper testOutputHelper, + Uri baseUri, + OrchardCoreUITestExecutorConfiguration configuration) => + StartAtataScopeAsync(testOutputHelper, baseUri, configuration).GetAwaiter().GetResult(); + + public static async Task StartAtataScopeAsync( ITestOutputHelper testOutputHelper, Uri baseUri, OrchardCoreUITestExecutorConfiguration configuration) @@ -33,7 +44,7 @@ public static AtataScope StartAtataScope( var builder = AtataContext.Configure() // The drivers are disposed when disposing AtataScope. #pragma warning disable CA2000 // Dispose objects before losing scope - .UseDriver(CreateDriver(browserConfiguration, timeoutConfiguration, testOutputHelper)) + .UseDriver(await CreateDriverAsync(browserConfiguration, timeoutConfiguration, testOutputHelper)) #pragma warning restore CA2000 // Dispose objects before losing scope .UseBaseUrl(baseUri.ToString()) .UseCulture(browserConfiguration.AcceptLanguage.ToString()) @@ -56,12 +67,12 @@ public static void SetupShellCliCommandFactory() => .UseCmdForWindows() .UseForOtherOS(new BashShellCliCommandFactory("-login")); - private static IWebDriver CreateDriver( + private static async Task CreateDriverAsync( BrowserConfiguration browserConfiguration, TimeoutConfiguration timeoutConfiguration, ITestOutputHelper testOutputHelper) { - IWebDriver From(Func factory) + Task FromAsync(Func> factory) where T : IWebDriver => factory(browserConfiguration, timeoutConfiguration.PageLoadTimeout); @@ -82,10 +93,10 @@ IWebDriver From(Func factory) { return browserConfiguration.Browser switch { - Browser.Chrome => From(WebDriverFactory.CreateChromeDriver), - Browser.Edge => From(WebDriverFactory.CreateEdgeDriver), - Browser.Firefox => From(WebDriverFactory.CreateFirefoxDriver), - Browser.InternetExplorer => From(WebDriverFactory.CreateInternetExplorerDriver), + Browser.Chrome => await FromAsync(WebDriverFactory.CreateChromeDriverAsync), + Browser.Edge => await FromAsync(WebDriverFactory.CreateEdgeDriverAsync), + Browser.Firefox => await FromAsync(WebDriverFactory.CreateFirefoxDriverAsync), + Browser.InternetExplorer => await FromAsync(WebDriverFactory.CreateInternetExplorerDriverAsync), _ => throw new InvalidOperationException($"Unknown browser: {browserConfiguration.Browser}."), }; } diff --git a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs index 0d280cf79..48199cdef 100644 --- a/Lombiq.Tests.UI/Services/UITestExecutionSession.cs +++ b/Lombiq.Tests.UI/Services/UITestExecutionSession.cs @@ -632,7 +632,7 @@ Task UITestingBeforeAppStartHandlerAsync(string contentRootPath, InstanceCommand _configuration.Events.AfterPageChange += TakeScreenshotIfEnabledAsync; } - var atataScope = AtataFactory.StartAtataScope(_testOutputHelper, uri, _configuration); + var atataScope = await AtataFactory.StartAtataScopeAsync(_testOutputHelper, uri, _configuration); return new UITestContext( contextId, diff --git a/Lombiq.Tests.UI/Services/WebDriverFactory.cs b/Lombiq.Tests.UI/Services/WebDriverFactory.cs index 0df9036cf..35570b69e 100644 --- a/Lombiq.Tests.UI/Services/WebDriverFactory.cs +++ b/Lombiq.Tests.UI/Services/WebDriverFactory.cs @@ -1,4 +1,5 @@ using Atata.WebDriverSetup; +using Lombiq.HelpfulLibraries.Cli.Helpers; using Lombiq.Tests.UI.Extensions; using OpenQA.Selenium; using OpenQA.Selenium.Chrome; @@ -8,6 +9,9 @@ using OpenQA.Selenium.IE; using System; using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; namespace Lombiq.Tests.UI.Services; @@ -15,9 +19,9 @@ public static class WebDriverFactory { private static readonly object _setupLock = new(); - public static ChromeDriver CreateChromeDriver(BrowserConfiguration configuration, TimeSpan pageLoadTimeout) + public static Task CreateChromeDriverAsync(BrowserConfiguration configuration, TimeSpan pageLoadTimeout) { - ChromeDriver CreateDriverInner(ChromeDriverService service) + Task CreateDriverInnerAsync(ChromeDriverService service) { var chromeConfig = new ChromeConfiguration { Options = new ChromeOptions().SetCommonOptions() }; @@ -45,25 +49,36 @@ ChromeDriver CreateDriverInner(ChromeDriverService service) // Helps with misconfigured hosts. if (chromeConfig.Service.HostName == "localhost") chromeConfig.Service.HostName = "127.0.0.1"; - return new ChromeDriver(chromeConfig.Service, chromeConfig.Options, pageLoadTimeout).SetCommonTimeouts(pageLoadTimeout); + return Task.FromResult( + new ChromeDriver(chromeConfig.Service, chromeConfig.Options, pageLoadTimeout) + .SetCommonTimeouts(pageLoadTimeout)); } var chromeWebDriverPath = Environment.GetEnvironmentVariable("CHROMEWEBDRIVER"); // #spell-check-ignore-line if (chromeWebDriverPath is { } driverPath && Directory.Exists(driverPath)) { - return CreateDriverInner(ChromeDriverService.CreateDefaultService(driverPath)); + return CreateDriverInnerAsync(ChromeDriverService.CreateDefaultService(driverPath)); } - return CreateDriver(BrowserNames.Chrome, () => CreateDriverInner(service: null)); + return CreateDriverAsync(BrowserNames.Chrome, () => CreateDriverInnerAsync(service: null)); } - public static EdgeDriver CreateEdgeDriver(BrowserConfiguration configuration, TimeSpan pageLoadTimeout) => - CreateDriver(BrowserNames.Edge, () => + public static Task CreateEdgeDriverAsync(BrowserConfiguration configuration, TimeSpan pageLoadTimeout) => + CreateDriverAsync(BrowserNames.Edge, async () => { var options = new EdgeOptions().SetCommonOptions(); options.SetCommonChromiumOptions(configuration); + // While the Edge driver easily locates Edge on Windows, it struggles on Linux, where the different release + // channels have different executable names. This setting looks up the "microsoft-edge-stable" command and + // sets the full path as the browser's binary location. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + (await CliWrapHelper.WhichAsync("microsoft-edge-stable"))?.FirstOrDefault() is { } binaryLocation) + { + options.BinaryLocation = binaryLocation.FullName; + } + configuration.BrowserOptionsConfigurator?.Invoke(options); var service = EdgeDriverService.CreateDefaultService(); @@ -72,7 +87,7 @@ public static EdgeDriver CreateEdgeDriver(BrowserConfiguration configuration, Ti return new EdgeDriver(service, options).SetCommonTimeouts(pageLoadTimeout); }); - public static FirefoxDriver CreateFirefoxDriver(BrowserConfiguration configuration, TimeSpan pageLoadTimeout) + public static Task CreateFirefoxDriverAsync(BrowserConfiguration configuration, TimeSpan pageLoadTimeout) { var options = new FirefoxOptions().SetCommonOptions(); @@ -89,11 +104,13 @@ public static FirefoxDriver CreateFirefoxDriver(BrowserConfiguration configurati configuration.BrowserOptionsConfigurator?.Invoke(options); - return CreateDriver(BrowserNames.Firefox, () => new FirefoxDriver(options).SetCommonTimeouts(pageLoadTimeout)); + return CreateDriverAsync( + BrowserNames.Firefox, + () => Task.FromResult(new FirefoxDriver(options).SetCommonTimeouts(pageLoadTimeout))); } - public static InternetExplorerDriver CreateInternetExplorerDriver(BrowserConfiguration configuration, TimeSpan pageLoadTimeout) => - CreateDriver(BrowserNames.InternetExplorer, () => + public static Task CreateInternetExplorerDriverAsync(BrowserConfiguration configuration, TimeSpan pageLoadTimeout) => + CreateDriverAsync(BrowserNames.InternetExplorer, () => { var options = new InternetExplorerOptions().SetCommonOptions(); @@ -101,7 +118,7 @@ public static InternetExplorerDriver CreateInternetExplorerDriver(BrowserConfigu options.AcceptInsecureCertificates = false; configuration.BrowserOptionsConfigurator?.Invoke(options); - return new InternetExplorerDriver(options).SetCommonTimeouts(pageLoadTimeout); + return Task.FromResult(new InternetExplorerDriver(options).SetCommonTimeouts(pageLoadTimeout)); }); private static TDriverOptions SetCommonOptions(this TDriverOptions driverOptions) @@ -160,13 +177,13 @@ private static TDriver SetCommonTimeouts(this TDriver driver, TimeSpan return driver; } - private static TDriver CreateDriver(string browserName, Func driverFactory) + private static async Task CreateDriverAsync(string browserName, Func> driverFactory) where TDriver : IWebDriver { try { - lock (_setupLock) DriverSetup.AutoSetUp(browserName); - return driverFactory(); + AutoSetup(browserName); + return await driverFactory(); } catch (Exception ex) { @@ -177,6 +194,13 @@ private static TDriver CreateDriver(string browserName, Func d } } + // We don't use the async version of auto setup because it doesn't do any locking. In fact it's just the sync method + // passed to Task.Run() so it wouldn't benefit us anyway. + private static void AutoSetup(string browserName) + { + lock (_setupLock) DriverSetup.AutoSetUp(browserName); + } + private sealed class ChromeConfiguration { public ChromeOptions Options { get; init; }