diff --git a/.gitignore b/.gitignore index 5fbcebe..8af2b54 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs -launchSettings.json # Build results [Dd]ebug[-\w]*/ diff --git a/.vsts-ci.yml b/.vsts-ci.yml index 82df15b..737a302 100644 --- a/.vsts-ci.yml +++ b/.vsts-ci.yml @@ -47,17 +47,11 @@ jobs: displayName: Use GitVersion - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 5.0.101' - inputs: - packageType: sdk - version: 5.0.101 - - - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.401' + displayName: 'Use .NET SDK' retryCountOnTaskFailure: 3 inputs: packageType: sdk - version: 6.0.401 + version: 7.0.302 - task: MSBuild@1 inputs: @@ -110,16 +104,11 @@ jobs: clean: true - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 5.0.101' - inputs: - packageType: sdk - version: 5.0.101 - - - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.401' + displayName: 'Use .NET SDK' + retryCountOnTaskFailure: 3 inputs: packageType: sdk - version: 6.0.401 + version: 7.0.302 - bash: | chmod +x build/wasm-uitest-run.sh diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 563cf0b..7006b81 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -19,6 +19,7 @@ true + diff --git a/src/Sample/Sample.UITests/Constants.cs b/src/Sample/Sample.UITests/Constants.cs index 4d862ff..74f168e 100644 --- a/src/Sample/Sample.UITests/Constants.cs +++ b/src/Sample/Sample.UITests/Constants.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading.Tasks; using Uno.UITest.Helpers.Queries; +using Uno.UITests.Helpers; namespace Sample.UITests { @@ -15,5 +16,7 @@ public class Constants public readonly static string iOSDeviceNameOrId = "iPad Pro (12.9-inch) (5th generation)"; public readonly static Platform CurrentPlatform = Platform.Browser; + + public readonly static Browser WebAssemblyBrowser = Browser.Chrome; } } diff --git a/src/Sample/Sample.UITests/DragCoordinates_Tests.cs b/src/Sample/Sample.UITests/DragCoordinates_Tests.cs index d0e5c51..95e5339 100644 --- a/src/Sample/Sample.UITests/DragCoordinates_Tests.cs +++ b/src/Sample/Sample.UITests/DragCoordinates_Tests.cs @@ -16,6 +16,8 @@ public class DragCoordinates_Tests : TestBase [Test] public void DragBorder01() { + App.Screenshot("home screen"); + Query testSelector = q => q.Marked("DragCoordinates 01"); Query rootCanvas = q => q.Marked("rootCanvas"); diff --git a/src/Sample/Sample.UITests/Sample.UITests.csproj b/src/Sample/Sample.UITests/Sample.UITests.csproj index bb4e95b..3c097c7 100644 --- a/src/Sample/Sample.UITests/Sample.UITests.csproj +++ b/src/Sample/Sample.UITests/Sample.UITests.csproj @@ -2,7 +2,6 @@ net47 - 7.3 diff --git a/src/Sample/Sample.UITests/TestBase.cs b/src/Sample/Sample.UITests/TestBase.cs index d607f85..815d7e7 100644 --- a/src/Sample/Sample.UITests/TestBase.cs +++ b/src/Sample/Sample.UITests/TestBase.cs @@ -40,6 +40,7 @@ public static void InitializeTestEnvrionment() AppInitializer.TestEnvironment.AndroidAppName = Constants.AndroidAppName; AppInitializer.TestEnvironment.iOSDeviceNameOrId = Constants.iOSDeviceNameOrId; AppInitializer.TestEnvironment.CurrentPlatform = Constants.CurrentPlatform; + AppInitializer.TestEnvironment.WebAssemblyBrowser = Constants.WebAssemblyBrowser; #if DEBUG AppInitializer.TestEnvironment.WebAssemblyHeadless = false; diff --git a/src/Sample/Sample.Wasm/Properties/launchSettings.json b/src/Sample/Sample.Wasm/Properties/launchSettings.json index d158dba..720f995 100644 --- a/src/Sample/Sample.Wasm/Properties/launchSettings.json +++ b/src/Sample/Sample.Wasm/Properties/launchSettings.json @@ -11,6 +11,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -18,10 +19,12 @@ "Sample.Wasm": { "commandName": "Project", "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "launchUrl": "http://localhost:55932/", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "http://localhost:5000" } } -} \ No newline at end of file +} diff --git a/src/Sample/Sample.Wasm/Sample.Wasm.csproj b/src/Sample/Sample.Wasm/Sample.Wasm.csproj index 32abcc0..98bd654 100644 --- a/src/Sample/Sample.Wasm/Sample.Wasm.csproj +++ b/src/Sample/Sample.Wasm/Sample.Wasm.csproj @@ -1,43 +1,49 @@  - - Exe - net6.0 - true - $(DefineConstants);__WASM__ - NU1701 - true - - - true - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + diff --git a/src/Uno.UITest.Helpers/AppInitializer.cs b/src/Uno.UITest.Helpers/AppInitializer.cs index f552ff4..31b93c3 100644 --- a/src/Uno.UITest.Helpers/AppInitializer.cs +++ b/src/Uno.UITest.Helpers/AppInitializer.cs @@ -179,29 +179,38 @@ private static IApp CreateBrowserApp(bool alreadyRunningApp) { var configurator = Uno.UITest.Selenium.ConfigureApp .WebAssembly - .Uri(new Uri(TestEnvironment.WebAssemblyDefaultUri)); - + .Uri(new Uri(TestEnvironment.WebAssemblyDefaultUri)) + .UsingBrowser(TestEnvironment.WebAssemblyBrowser.ToString()); if(!string.IsNullOrEmpty(TestEnvironment.ChromeDriverPath)) { - configurator = configurator.ChromeDriverLocation( - Path.Combine(TestContext.CurrentContext.TestDirectory, - TestEnvironment.ChromeDriverPath.Replace('\\', Path.DirectorySeparatorChar))); + var driverPath = Path.Combine( + TestContext.CurrentContext.TestDirectory, + TestEnvironment.ChromeDriverPath.Replace('\\', Path.DirectorySeparatorChar)); + configurator = configurator.DriverPath(driverPath); + } + else if(!string.IsNullOrEmpty(TestEnvironment.SeleniumDriverPath)) + { + var driverPath = Path.Combine( + TestContext.CurrentContext.TestDirectory, + TestEnvironment.SeleniumDriverPath.Replace('\\', Path.DirectorySeparatorChar)); + configurator = configurator.DriverPath(driverPath); } - if(!TestEnvironment.WebAssemblyHeadless) + if(TestEnvironment.WebAssemblyHeadless) { configurator = configurator - .Headless(false) - .SeleniumArgument("--remote-debugging-port=9222"); + .Headless(true); } else { configurator = configurator - .Headless(true); + .Headless(false) + .SeleniumArgument("--remote-debugging-port=9222"); } - _currentApp = configurator.ScreenShotsPath(TestContext.CurrentContext.TestDirectory) + _currentApp = configurator + .ScreenShotsPath(TestContext.CurrentContext.TestDirectory) .StartApp(); return _currentApp; diff --git a/src/Uno.UITest.Helpers/AppInitializerEnvironment.cs b/src/Uno.UITest.Helpers/AppInitializerEnvironment.cs index 124389d..93885bd 100644 --- a/src/Uno.UITest.Helpers/AppInitializerEnvironment.cs +++ b/src/Uno.UITest.Helpers/AppInitializerEnvironment.cs @@ -45,9 +45,26 @@ internal AppInitializerEnvironment() /// public string ChromeDriverPath { get; set; } + /// + /// Defines the location of selenium driver. + /// + /// + /// If not defined, the test engine will select the version based on + /// the currently installed browsers version. + /// + public string SeleniumDriverPath { get; set; } + /// /// Defines if the browser tests are running in chrome without a window. /// public bool WebAssemblyHeadless { get; set; } = true; + + /// + /// Defines the browser to use for the Web platform. Cf. remarks about compatibility + /// + /// + /// Note that all browser does not supports all options defined here. For instance Edge does support only the . + /// + public Browser WebAssemblyBrowser { get; set; } = Browser.Chrome; } } diff --git a/src/Uno.UITest.Helpers/Browser.cs b/src/Uno.UITest.Helpers/Browser.cs new file mode 100644 index 0000000..6e5856b --- /dev/null +++ b/src/Uno.UITest.Helpers/Browser.cs @@ -0,0 +1,18 @@ +using System; +using System.Linq; + +namespace Uno.UITests.Helpers +{ + /// + /// Supported web browsers for UI tests + /// + public enum Browser + { + Chrome, + + /// + /// The **CHROMIUM Based** Edge web browser + /// + Edge + } +} diff --git a/src/Uno.UITest.Puppeteer/ChromeAppConfigurator.cs b/src/Uno.UITest.Puppeteer/ChromeAppConfigurator.cs deleted file mode 100644 index 9df7f52..0000000 --- a/src/Uno.UITest.Puppeteer/ChromeAppConfigurator.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Uno.UITest.Selenium -{ - public class ChromeAppConfigurator - { - internal string ChromeDriverPath { get; private set; } - internal Uri SiteUri { get; private set; } - internal string InternalScreenShotsPath { get; private set; } = ""; - internal bool InternalHeadless { get; private set; } = true; - internal int InternalWindowWidth { get; private set; } = 1024; - internal int InternalWindowHeight { get; private set; } = 768; - internal string InternalBrowserBinaryPath { get; private set; } - internal List InternalSeleniumArgument = new List(); - internal bool InternalDetectDockerEnvironment = true; - - public ChromeAppConfigurator() - { - } - - public ChromeAppConfigurator Uri(Uri uri) { SiteUri = uri; return this; } - - public ChromeAppConfigurator ChromeDriverLocation(string chromeDriverPath) { ChromeDriverPath = chromeDriverPath; return this; } - - public ChromeAppConfigurator ScreenShotsPath(string path) { InternalScreenShotsPath = path; return this; } - - public ChromeAppConfigurator BrowserBinaryPath(string path) { InternalBrowserBinaryPath = path; return this; } - - /// - /// This parameters allows to provide a set of additional parameters to be provided to the WebDriver. - /// - public ChromeAppConfigurator SeleniumArgument(string argument) { InternalSeleniumArgument.Add(argument); return this; } - - - /// - /// Enables the detection of the docker environment to configure the WebDriver accordingly. Enabled by default. - /// - public ChromeAppConfigurator DetectDockerEnvironment(bool enabled) { InternalDetectDockerEnvironment = enabled; return this; } - - /// - /// Runs the browser as headless. Defaults to true. - /// - /// - /// - public ChromeAppConfigurator Headless(bool isHeadless) { InternalHeadless = isHeadless; return this; } - - - /// - /// Sets the window size. Defaults to 1024x768 - /// - /// - /// - /// - public ChromeAppConfigurator WindowSize(int width, int height) - { - InternalWindowWidth = width; - InternalWindowHeight = height; - return this; - } - - public IApp StartApp() => new SeleniumApp(this); - } -} diff --git a/src/Uno.UITest.Puppeteer/ConfigureApp.cs b/src/Uno.UITest.Puppeteer/ConfigureApp.cs index 60a4e62..9d6f843 100644 --- a/src/Uno.UITest.Puppeteer/ConfigureApp.cs +++ b/src/Uno.UITest.Puppeteer/ConfigureApp.cs @@ -6,7 +6,6 @@ namespace Uno.UITest.Selenium { public static class ConfigureApp { - public static ChromeAppConfigurator WebAssembly => - new ChromeAppConfigurator(); + public static SeleniumAppConfigurator WebAssembly => new SeleniumAppConfigurator(); } } diff --git a/src/Uno.UITest.Puppeteer/Models/ChromeDriverVersionModels.cs b/src/Uno.UITest.Puppeteer/Models/ChromeDriverVersionModels.cs new file mode 100644 index 0000000..78aa034 --- /dev/null +++ b/src/Uno.UITest.Puppeteer/Models/ChromeDriverVersionModels.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace Uno.UITest.Selenium.Models +{ + + // Models generated by json2csharp.com from https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json + public class Chrome + { + [JsonPropertyName("platform")] + public string Platform { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } + } + + public class Chromedriver + { + [JsonPropertyName("platform")] + public string Platform { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } + } + + public class Downloads + { + [JsonPropertyName("chrome")] + public Chrome[] Chrome { get; set; } + + [JsonPropertyName("chromedriver")] + public Chromedriver[] Chromedriver { get; set; } + } + + public class Root + { + [JsonPropertyName("timestamp")] + public DateTime? Timestamp { get; set; } + + [JsonPropertyName("versions")] + public ChromeVersion[] Versions { get; set; } + } + + public class ChromeVersion + { + [JsonPropertyName("version")] + public string Version { get; set; } + + [JsonPropertyName("revision")] + public string Revision { get; set; } + + [JsonPropertyName("downloads")] + public Downloads Downloads { get; set; } + } + +} diff --git a/src/Uno.UITest.Puppeteer/SeleniumApp.cs b/src/Uno.UITest.Puppeteer/SeleniumApp.cs index 5d0221c..9412285 100644 --- a/src/Uno.UITest.Puppeteer/SeleniumApp.cs +++ b/src/Uno.UITest.Puppeteer/SeleniumApp.cs @@ -3,15 +3,12 @@ using System.Diagnostics; using System.Globalization; using System.IO; -using System.IO.Compression; using System.Linq; -using System.Net; -using System.Net.Http; -using System.Security.Policy; using System.Threading; using System.Threading.Tasks; using OpenQA.Selenium; using OpenQA.Selenium.Chrome; +using OpenQA.Selenium.Chromium; using OpenQA.Selenium.Interactions; using OpenQA.Selenium.Remote; using static System.Math; @@ -20,69 +17,14 @@ namespace Uno.UITest.Selenium { public partial class SeleniumApp : IApp { - private const string UNO_UITEST_DRIVERPATH_CHROME = "UNO_UITEST_DRIVERPATH_CHROME"; - - - private readonly ChromeDriver _driver; - private string _screenShotPath; + private readonly ChromiumDriver _driver; + private readonly string _screenShotPath; private readonly TimeSpan DefaultRetry = TimeSpan.FromMilliseconds(500); private readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(1); - - public SeleniumApp(ChromeAppConfigurator config) + public SeleniumApp(ChromiumDriver driver, string screenShotPath) { - var targetUri = GetEnvironmentVariable("UNO_UITEST_TARGETURI", config.SiteUri.OriginalString); - var driverPath = GetEnvironmentVariable(UNO_UITEST_DRIVERPATH_CHROME, config.ChromeDriverPath); - var screenShotPath = GetEnvironmentVariable("UNO_UITEST_SCREENSHOT_PATH", config.InternalScreenShotsPath); - var chromeBinPath = GetEnvironmentVariable("UNO_UITEST_CHROME_BINARY_PATH", config.InternalBrowserBinaryPath); - - var options = new ChromeOptions(); - - if(config.InternalHeadless) - { - options.AddArguments("--no-sandbox"); - options.AddArguments("--disable-dev-shm-usage"); - options.AddArgument("headless"); - } - - options.AddArgument($"window-size={config.InternalWindowWidth}x{config.InternalWindowHeight}"); - - if(config.InternalDetectDockerEnvironment) - { - if(File.Exists("/.dockerenv")) - { - // When running under docker, ports bindings may not work properly - // as the current local host may not be detected properly by the web driver - // causing errors like this one: - // - // [SEVERE]: bind() returned an error, errno=99: Cannot assign requested address (99) - // - // When InternalDetectDockerEnvironment is set, tell the daemon to listen on - // all available interfaces - Console.WriteLine($"Container mode enabled, adding whitelisted-ips"); - options.AddArguments("--whitelisted-ips"); - } - } - - foreach(var arg in config.InternalSeleniumArgument) - { - options.AddArguments(arg); - } - - if(!string.IsNullOrEmpty(chromeBinPath)) - { - options.BinaryLocation = chromeBinPath; - } - - if(string.IsNullOrEmpty(driverPath)) - { - driverPath = TryDownloadChromeDriver(); - } - - options.SetLoggingPreference(LogType.Browser, LogLevel.All); - - _driver = new ChromeDriver(driverPath, options); - _driver.Url = targetUri; + _driver = driver; _screenShotPath = screenShotPath; } @@ -92,145 +34,7 @@ IQueryable IApp.GetSystemLogs(DateTime? afterDate) .Where(entry => afterDate == null || entry.Timestamp > afterDate) .Select(entry => new SeleniumLogEntry(entry)); - private string TryDownloadChromeDriver() - { - if(Environment.OSVersion.Platform == PlatformID.Win32NT) - { - var chromePath = $@"{Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86)}\Google\Chrome\Application\chrome.exe"; - // Chrome might be installed in C:\Program Files\Google... - // If file doesn't exist, check there. - if(!File.Exists(chromePath)) - { - // Using environment variable here since EnvironMent.SpecialFolder.ProgramFiles resolves to the X86 - // variant depending on the executable architecture. The path variable always evaluates to the correct path though. - chromePath = $@"{Environment.GetEnvironmentVariable("ProgramW6432")}\Google\Chrome\Application\chrome.exe"; - } - chromePath = chromePath.Replace("\\", "\\\\"); - - var process = new Process(); - process.StartInfo.FileName = "wmic.exe"; - process.StartInfo.Arguments = $@"datafile where name=""{chromePath}"" get Version /value"; - process.StartInfo.UseShellExecute = false; - process.StartInfo.RedirectStandardOutput = true; - process.StartInfo.RedirectStandardError = true; - process.Start(); - - var wincOutput = process.StandardOutput.ReadToEnd(); - var chromeRawVersion = wincOutput.Split('=').LastOrDefault()?.Trim(); - - if(Version.TryParse(chromeRawVersion, out var chromeVersion)) - { - var driverLocalPath = Path.Combine(Path.GetTempPath(), "Uno.UITests", "ChromeDriver", chromeVersion.ToString()); - Directory.CreateDirectory(driverLocalPath); - - var driverPath = Path.Combine(driverLocalPath, "chromedriver.exe"); - - if(!File.Exists(driverPath)) - { - // Chrome driver selection: http://chromedriver.chromium.org/downloads/version-selection - - var chromeDriverLatestVersionUri = $"https://chromedriver.storage.googleapis.com/LATEST_RELEASE_{chromeVersion.Major}"; - - Console.WriteLine($"Fetching Chrome driver version for Chrome [{chromeRawVersion}]"); -#if NET6_0_OR_GREATER - var driverVersion = Task.Run(async () => - { - using var client = new HttpClient(); - return await client.GetStringAsync(chromeDriverLatestVersionUri); - }).Result; -#else - var driverVersion = new WebClient().DownloadString(chromeDriverLatestVersionUri); -#endif - - var chromeDriverVersionUri = $"https://chromedriver.storage.googleapis.com/{driverVersion}/chromedriver_win32.zip"; - - var tempZipFileName = Path.GetTempFileName(); - - try - { - Console.WriteLine($"Downloading Chrome driver from [{chromeDriverVersionUri}]"); -#if NET6_0_OR_GREATER - Task.Run(async () => - { - using var client = new HttpClient(); - using var response = await client.GetAsync(chromeDriverVersionUri); - if(!response.IsSuccessStatusCode) - { - return; - } - - var fileInfo = new FileInfo(tempZipFileName); - if(!fileInfo.Directory.Exists) - { - fileInfo.Directory.Create(); - } - if(fileInfo.Exists) - { - fileInfo.Delete(); - } - - using var writer = fileInfo.OpenWrite(); - using var responseStream = await response.Content.ReadAsStreamAsync(); - await responseStream.CopyToAsync(writer); - }).Wait(); -#else - new WebClient().DownloadFile(chromeDriverVersionUri, tempZipFileName); -#endif - - using(var zipFile = ZipFile.OpenRead(tempZipFileName)) - { - zipFile.GetEntry("chromedriver.exe").ExtractToFile(driverPath, true); - } - } - finally - { - if(File.Exists(tempZipFileName)) - { - File.Delete(tempZipFileName); - } - } - } - - return Path.GetDirectoryName(driverPath); - } - else - { - throw new NotSupportedException($"Unable to determine the chrome driver version. The used path was [{chromePath}], found [{chromeVersion}]."); - } - } - else - { - throw new NotSupportedException($"Unable to determine the chrome driver location. Use the {UNO_UITEST_DRIVERPATH_CHROME} environment variable."); - } - } - - private string GetEnvironmentVariable(string variableName, string defaultValue) - { - var value = Environment.GetEnvironmentVariable(variableName); - - var hasValue = !string.IsNullOrWhiteSpace(value); - - if(hasValue) - { - Console.WriteLine($"Overriding value with {variableName} = {value}"); - } - - return hasValue ? value : defaultValue; - } - - private bool GetEnvironmentVariable(string variableName, bool defaultValue) - { - var value = Environment.GetEnvironmentVariable(variableName); - - var hasValue = bool.TryParse(value, out var varValue); - - if(hasValue) - { - Console.WriteLine($"Overriding value with {variableName} = {value}"); - } - - return hasValue ? varValue : defaultValue; - } + public static SeleniumDriverManager SelectedBrowser { get; set; } void PerformActions(Action action) { @@ -490,7 +294,6 @@ void IApp.Tap(Func query) void IApp.TapCoordinates(float x, float y) { - PerformActions(a => a .MoveToElement(_driver.FindElement(By.TagName("body")), 0, 0) .MoveByOffset((int)x, (int)y) @@ -680,6 +483,5 @@ private IWebElement GetSingleElement(Func query) throw new InvalidOperationException($"Invalid query results"); } - } } diff --git a/src/Uno.UITest.Puppeteer/SeleniumAppConfigurator.cs b/src/Uno.UITest.Puppeteer/SeleniumAppConfigurator.cs new file mode 100644 index 0000000..b213cf9 --- /dev/null +++ b/src/Uno.UITest.Puppeteer/SeleniumAppConfigurator.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.IO; +using OpenQA.Selenium.Chrome; +using OpenQA.Selenium.Chromium; +using OpenQA.Selenium.Edge; +using OpenQA.Selenium.Remote; + +namespace Uno.UITest.Selenium +{ + public class SeleniumAppConfigurator + { + internal const string UNO_UITEST_DRIVER_PATH = "UNO_UITEST_DRIVER_PATH"; + + private string _browser; + private Uri SiteUri { get; set; } + private string InternalScreenShotsPath { get; set; } = ""; + private string InternalDriverPath { get; set; } + private string InternalBrowserPath { get; set; } + private bool InternalHeadless { get; set; } = true; + private int InternalWindowWidth { get; set; } = 1024; + private int InternalWindowHeight { get; set; } = 768; + private List InternalSeleniumArgument = new List(); + private bool InternalDetectDockerEnvironment = true; + + #region Fluent declaration + public SeleniumAppConfigurator UsingBrowser(string browser) + { + _browser = browser; + return this; + } + + public SeleniumAppConfigurator Uri(Uri uri) + { + SiteUri = uri; + return this; + } + + public SeleniumAppConfigurator ScreenShotsPath(string path) + { + InternalScreenShotsPath = path; + return this; + } + + public SeleniumAppConfigurator BrowserBinaryPath(string path) + { + InternalBrowserPath = path; + return this; + } + + public SeleniumAppConfigurator BrowserPath(string path) + { + InternalBrowserPath = path; + return this; + } + + public SeleniumAppConfigurator DriverPath(string driverPath) + { + InternalDriverPath = driverPath; + return this; + } + + /// + /// This parameters allows to provide a set of additional parameters to be provided to the WebDriver. + /// + public SeleniumAppConfigurator SeleniumArgument(string argument) + { + InternalSeleniumArgument.Add(argument); + return this; + } + + + /// + /// Enables the detection of the docker environment to configure the WebDriver accordingly. Enabled by default. + /// + public SeleniumAppConfigurator DetectDockerEnvironment(bool enabled) + { + InternalDetectDockerEnvironment = enabled; + return this; + } + + /// + /// Runs the browser as headless. Defaults to true. + /// + /// + /// + public SeleniumAppConfigurator Headless(bool isHeadless) + { + InternalHeadless = isHeadless; + return this; + } + + /// + /// Sets the window size. Defaults to 1024x768 + /// + /// + /// + /// + public SeleniumAppConfigurator WindowSize(int width, int height) + { + InternalWindowWidth = width; + InternalWindowHeight = height; + return this; + } + #endregion + + public IApp StartApp() + { + var targetUri = GetEnvironmentVariable("UNO_UITEST_TARGETURI", SiteUri.OriginalString); + var screenShotPath = GetEnvironmentVariable("UNO_UITEST_SCREENSHOT_PATH", InternalScreenShotsPath); + var browser = GetEnvironmentVariable("UNO_UITEST_BROWSER", _browser); + var browserPath = GetEnvironmentVariable("UNO_UITEST_BROWSER_PATH", InternalBrowserPath); + var driverPath = GetEnvironmentVariable(UNO_UITEST_DRIVER_PATH, InternalDriverPath); + + ChromiumDriver driver; + switch(browser?.ToUpperInvariant()) + { + case "EDGE": + driver = GetEdgeDriver(browserPath, driverPath); + break; + + case "CHROME": + driver = GetChromeDriver(browserPath, driverPath); + break; + + default: + if(browserPath?.Contains("edge") ?? driverPath?.Contains("edge") ?? false) + { + driver = GetEdgeDriver(browserPath, driverPath); + } + else + { + driver = GetChromeDriver(browserPath, driverPath); + } + break; + } + driver.Url = targetUri; + + return new SeleniumApp(driver, screenShotPath); + } + + protected ChromiumDriver GetChromeDriver(string browserPath = null, string driverPath = null) + { + // For backward compatibility, we give priority to the "CHROME" specific env. variables + driverPath = GetEnvironmentVariable("UNO_UITEST_DRIVERPATH_CHROME", driverPath); + browserPath = GetEnvironmentVariable("UNO_UITEST_CHROME_BINARY_PATH", browserPath); + + var options = new ChromeOptions(); + ApplyOptions(options, browserPath); + + var driver = string.IsNullOrEmpty(driverPath) + ? SeleniumDriverManager.Chrome.FromChromePath(browserPath, options) + : SeleniumDriverManager.Chrome.FromDriverPath(driverPath, options); + + return driver; + } + + protected ChromiumDriver GetEdgeDriver(string browserPath = null, string driverPath = null) + { + var options = new EdgeOptions(); + ApplyOptions(options, browserPath); + + var driver = string.IsNullOrEmpty(driverPath) + ? SeleniumDriverManager.Edge.FromEdgePath(browserPath, options) + : SeleniumDriverManager.Edge.FromDriverPath(driverPath, options); + + return driver; + } + + private void ApplyOptions(ChromiumOptions options, string browserPath) + { + if(InternalHeadless) + { + options.AddArguments("--no-sandbox"); + options.AddArgument("headless"); + } + + options.AddArgument($"window-size={InternalWindowWidth}x{InternalWindowHeight}"); + + if(InternalDetectDockerEnvironment) + { + if(File.Exists("/.dockerenv")) + { + // When running under docker, ports bindings may not work properly + // as the current local host may not be detected properly by the web driver + // causing errors like this one: + // + // [SEVERE]: bind() returned an error, errno=99: Cannot assign requested address (99) + // + // When InternalDetectDockerEnvironment is set, tell the daemon to listen on + // all available interfaces + Console.WriteLine($"Container mode enabled, adding whitelisted-ips"); + options.AddArguments("--whitelisted-ips"); + } + } + + foreach(var arg in InternalSeleniumArgument) + { + options.AddArguments(arg); + } + + if(!string.IsNullOrEmpty(browserPath)) + { + options.BinaryLocation = browserPath; + } + } + + #region Helpers + protected string GetEnvironmentVariable(string variableName, string defaultValue) + { + var value = Environment.GetEnvironmentVariable(variableName); + var hasValue = !string.IsNullOrWhiteSpace(value); + + if(hasValue) + { + Console.WriteLine($"Overriding value with {variableName} = {value}"); + } + + return hasValue ? value : defaultValue; + } + + protected bool GetEnvironmentVariable(string variableName, bool defaultValue) + { + var value = Environment.GetEnvironmentVariable(variableName); + + var hasValue = bool.TryParse(value, out var varValue); + + if(hasValue) + { + Console.WriteLine($"Overriding value with {variableName} = {value}"); + } + + return hasValue ? varValue : defaultValue; + } + #endregion + } +} diff --git a/src/Uno.UITest.Puppeteer/SeleniumDriverManager.cs b/src/Uno.UITest.Puppeteer/SeleniumDriverManager.cs new file mode 100644 index 0000000..f583ce8 --- /dev/null +++ b/src/Uno.UITest.Puppeteer/SeleniumDriverManager.cs @@ -0,0 +1,231 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Text.Json; +using OpenQA.Selenium.Chrome; +using OpenQA.Selenium.Edge; + +namespace Uno.UITest.Selenium +{ + public class SeleniumDriverManager + { + public static class Chrome + { + // Chrome driver selection: http://chromedriver.chromium.org/downloads/version-selection + + public static ChromeDriver FromChromePath(string chromePath, ChromeOptions options) + => new ChromeDriver( + new SeleniumDriverManager("chromedriver").GetOrDownloadLatestDriverForBin( + ChromeFilePath(), + GetDriverLatestVersion, + GetDriverUri).FullName, + options); + + private static string ChromeFilePath() + { + var chromePath = $@"{Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86)}\Google\Chrome\Application\chrome.exe"; + // Chrome might be installed in C:\Program Files\Google... + // If file doesn't exist, check there. + if(!File.Exists(chromePath)) + { + // Using environment variable here since EnvironMent.SpecialFolder.ProgramFiles resolves to the X86 + // variant depending on the executable architecture. The path variable always evaluates to the correct path though. + chromePath = $@"{Environment.GetEnvironmentVariable("ProgramW6432")}\Google\Chrome\Application\chrome.exe"; + } + return chromePath; + } + + public static ChromeDriver FromDriverPath(string driverPath, ChromeOptions options) + => new ChromeDriver(driverPath, options); + + private static Uri GetDriverLatestVersion(Version browserVersion) => + browserVersion.Major <= 114 ? + new Uri($"https://chromedriver.storage.googleapis.com/LATEST_RELEASE_{browserVersion.Major}") : + new Uri("https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json"); + + private static Uri GetDriverUri(Version browserVersion, string driverVersion) + { + if(browserVersion.Major <= 114) + { + return new Uri($"https://chromedriver.storage.googleapis.com/{driverVersion}/chromedriver_win32.zip"); + } + else + { + var driverInfo = JsonSerializer.Deserialize(driverVersion); + return new Uri((from v in driverInfo.Versions + let ver = Version.TryParse(v.Version, out var parsedVersion) ? parsedVersion : default + where ver.Major == browserVersion.Major && + ver.Minor == browserVersion.Minor && + (v.Downloads?.Chromedriver?.Any() ?? false) + orderby ver descending + from platform in v.Downloads.Chromedriver + where platform.Platform == "win32" + select platform.Url).First()); + } + } + + } + + public static class Edge + { + // Edge driver selection https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/#downloads + // Edge driver documentation https://docs.microsoft.com/en-us/microsoft-edge/webdriver-chromium + + public static EdgeDriver FromEdgePath(string edgePath, EdgeOptions options) + { + edgePath = edgePath ?? $@"{Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86)}\Microsoft\Edge\Application\msedge.exe"; + // Edge might be installed in C:\Program Files\Edge... + // If file doesn't exist, check there. + if(!File.Exists(edgePath)) + { + // Using environment variable here since EnvironMent.SpecialFolder.ProgramFiles resolves to the X86 + // variant depending on the executable architecture. The path variable always evaluates to the correct path though. + edgePath = $@"{Environment.GetEnvironmentVariable("ProgramW6432")}\Microsoft\Edge\Application\msedge.exe"; + } + + options.BinaryLocation = edgePath; + + var manager = new SeleniumDriverManager("msedgedriver"); + var driverPath = manager.GetOrDownloadLatestDriverForBin(edgePath, null, /*GetDriverLatestVersion, */GetDriverUri); + + var svc = EdgeDriverService.CreateDefaultService(driverPath.FullName);//.CreateDefaultServiceFromOptions(driverPath.FullName, "msedgedriver.exe", options); + svc.EnableVerboseLogging = true; + + var driver = new EdgeDriver(svc, options); + + return driver; + } + + public static EdgeDriver FromDriverPath(string driverPath, EdgeOptions options) + => new EdgeDriver(driverPath, options); + + private static Uri GetDriverLatestVersion(Version browserVersion) => new Uri($"https://msedgedriver.azureedge.net/LATEST_RELEASE_{browserVersion.Major}"); + private static Uri GetDriverUri(Version browserVersion, string driverVersion) => new Uri($"https://msedgedriver.azureedge.net/{driverVersion}/edgedriver_win32.zip"); + } + + private SeleniumDriverManager(string driverName) + { + DriverName = driverName; + } + + public string DriverName { get; } + + /// + /// Gets the file version of the provided browser path + /// + /// Path to the browser executable + protected Version GetVersion(string browserPath) + { + var process = new Process(); + process.StartInfo.FileName = "wmic.exe"; + process.StartInfo.Arguments = $@"datafile where name=""{browserPath.Replace("\\", "\\\\")}"" get Version /value"; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.Start(); + + var wincOutput = process.StandardOutput.ReadToEnd(); + var browserRawVersion = wincOutput.Split('=').LastOrDefault()?.Trim(); + + if(Version.TryParse(browserRawVersion, out var browserVersion)) + { + return browserVersion; + } + else + { + throw new NotSupportedException($"Unable to determine the browser version. The used path was [{browserPath}], found raw [{browserRawVersion}] parsed [{browserVersion}]."); + } + } + + /// + /// Gets the target standard install path for the given version of this driver + /// + /// The version of the driver + protected FileInfo GetDriverInstallPath(Version version) + { + var driverLocalPath = Path.Combine(Path.GetTempPath(), "Uno.UITests", $"{DriverName}", version.ToString()); + Directory.CreateDirectory(driverLocalPath); + + return new FileInfo(driverLocalPath + $"\\{DriverName}.exe"); + } + + /// + /// Download the zip package of the driver, and extract the driver executable to the target install path. + /// + /// The Uri of the package of the driver to download + /// The target install path of the driver + protected void Download(Uri driverSourceUri, FileInfo driverInstallPath) + { + var tempZipFileName = Path.GetTempFileName(); + try + { + Console.WriteLine($"Downloading {DriverName} from [{driverSourceUri.OriginalString}]"); + new WebClient().DownloadFile(driverSourceUri, tempZipFileName); + + using(var zipFile = ZipFile.OpenRead(tempZipFileName)) + { + zipFile.Entries + .FirstOrDefault(x => x.Name.EndsWith($"{DriverName}.exe"))? + .ExtractToFile(driverInstallPath.FullName, true); + } + } + finally + { + try + { + if(File.Exists(tempZipFileName)) + { + File.Delete(tempZipFileName); + } + } + catch + { // Make sure if the file is locked process doesn't crash + } + } + } + + protected delegate Uri GetDriverLatestVersion(Version browserVersion); + protected delegate Uri GetDriverUri(Version browserVersion, string driverVersion); + + protected DirectoryInfo GetOrDownloadLatestDriverForBin( + string binPath, + GetDriverLatestVersion getDriverLatestVersion, + GetDriverUri getDriverUri) + { + if(Environment.OSVersion.Platform == PlatformID.Win32NT) + { + var version = GetVersion(binPath); + var driverFile = GetDriverInstallPath(version); + + if(!driverFile.Exists) + { + string driverVersion; + if(getDriverLatestVersion == null) + { + driverVersion = version.ToString(); + } + else + { + var driverLatestVersion = getDriverLatestVersion(version); + + Console.WriteLine($"Fetching driver version for {DriverName} [{version}]"); + driverVersion = new WebClient().DownloadString(driverLatestVersion).Trim(); + } + + var driverUri = getDriverUri(version, driverVersion); + + Download(driverUri, driverFile); + } + + return driverFile.Directory; + } + else + { + throw new NotSupportedException($"Unable to determine the chrome driver location. Use the {SeleniumAppConfigurator.UNO_UITEST_DRIVER_PATH} environment variable."); + } + } + } +} diff --git a/src/Uno.UITest.Puppeteer/Uno.UITest.Selenium.csproj b/src/Uno.UITest.Puppeteer/Uno.UITest.Selenium.csproj index a0890a4..2aabcf3 100644 --- a/src/Uno.UITest.Puppeteer/Uno.UITest.Selenium.csproj +++ b/src/Uno.UITest.Puppeteer/Uno.UITest.Selenium.csproj @@ -22,6 +22,7 @@ + diff --git a/src/uno.uitest.sln b/src/uno.uitest.sln index 96d7c8a..7f7a97a 100644 --- a/src/uno.uitest.sln +++ b/src/uno.uitest.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28902.138 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34003.232 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Uno.UITest", "uno.uitest\Uno.UITest.csproj", "{C0578553-D584-48C3-9484-1A25DAD66269}" EndProject @@ -25,14 +25,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Wasm", "Sample\Sampl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Uno.UITest.Helpers", "Uno.UITest.Helpers\Uno.UITest.Helpers.csproj", "{B233D12F-E9FD-4239-9742-A2F712752000}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C6491A5A-8CBB-493C-A8EF-EFF6815F7550}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + EndProjectSection +EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - Sample\Sample.Shared\Sample.Shared.projitems*{336cc69a-c9d1-48d3-8029-d80d8989403e}*SharedItemsImports = 4 - Sample\Sample.Shared\Sample.Shared.projitems*{34403e87-8f3e-40c3-8ece-7f3873bcd693}*SharedItemsImports = 4 - Sample\Sample.Shared\Sample.Shared.projitems*{6279c845-92f8-4333-ab99-3d213163593c}*SharedItemsImports = 13 - Sample\Sample.Shared\Sample.Shared.projitems*{74eebcc9-f40e-422b-8c39-ca6e674301ae}*SharedItemsImports = 5 - Sample\Sample.Shared\Sample.Shared.projitems*{dbce58a5-d2d6-4912-abd4-aa7e12519361}*SharedItemsImports = 4 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU Ad-Hoc|ARM = Ad-Hoc|ARM @@ -525,4 +524,11 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8151C9E9-B16D-46FF-B9AB-8E19B32E1B4F} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + Sample\Sample.Shared\Sample.Shared.projitems*{336cc69a-c9d1-48d3-8029-d80d8989403e}*SharedItemsImports = 4 + Sample\Sample.Shared\Sample.Shared.projitems*{34403e87-8f3e-40c3-8ece-7f3873bcd693}*SharedItemsImports = 4 + Sample\Sample.Shared\Sample.Shared.projitems*{6279c845-92f8-4333-ab99-3d213163593c}*SharedItemsImports = 13 + Sample\Sample.Shared\Sample.Shared.projitems*{74eebcc9-f40e-422b-8c39-ca6e674301ae}*SharedItemsImports = 5 + Sample\Sample.Shared\Sample.Shared.projitems*{dbce58a5-d2d6-4912-abd4-aa7e12519361}*SharedItemsImports = 4 + EndGlobalSection EndGlobal