Merge pull request #35 from unoplatform/dev/dr/Edge

feat(EdgeSupport): Add support for the chromium edge
This commit is contained in:
Nick Randolph 2023-08-10 16:48:43 +10:00 коммит произвёл GitHub
Родитель 64c6caccd4 bfdca37f6d
Коммит 9669fd2783
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
20 изменённых файлов: 664 добавлений и 345 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -13,7 +13,6 @@
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
launchSettings.json
# Build results
[Dd]ebug[-\w]*/

Просмотреть файл

@ -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'
displayName: 'Use .NET SDK'
retryCountOnTaskFailure: 3
inputs:
packageType: sdk
version: 5.0.101
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.401'
inputs:
packageType: sdk
version: 6.0.401
version: 7.0.302
- bash: |
chmod +x build/wasm-uitest-run.sh

Просмотреть файл

@ -19,6 +19,7 @@
<!-- Force embedded roslyn generators -->
<UnoUIUseRoslynSourceGenerators>true</UnoUIUseRoslynSourceGenerators>
<!--<LangVersion>10.0</LangVersion>-->
</PropertyGroup>
<Choose>

Просмотреть файл

@ -4,6 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Uno.UITest.Helpers.Queries;
using Uno.UITests.Helpers;
namespace Sample.UITests
{
@ -15,5 +16,7 @@ namespace Sample.UITests
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;
}
}

Просмотреть файл

@ -16,6 +16,8 @@ namespace Sample.UITests
[Test]
public void DragBorder01()
{
App.Screenshot("home screen");
Query testSelector = q => q.Marked("DragCoordinates 01");
Query rootCanvas = q => q.Marked("rootCanvas");

Просмотреть файл

@ -2,7 +2,6 @@
<PropertyGroup>
<TargetFramework>net47</TargetFramework>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<ItemGroup>

Просмотреть файл

@ -40,6 +40,7 @@ namespace Sample.UITests
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;

Просмотреть файл

@ -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"
}
}
}
}

Просмотреть файл

@ -1,43 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<WasmHead>true</WasmHead>
<DefineConstants>$(DefineConstants);__WASM__</DefineConstants>
<NoWarn>NU1701</NoWarn>
<UnoUIUseRoslynSourceGenerators>true</UnoUIUseRoslynSourceGenerators>
</PropertyGroup>
<PropertyGroup>
<IsUiAutomationMappingEnabled>true</IsUiAutomationMappingEnabled>
</PropertyGroup>
<ItemGroup>
<Content Include="..\Sample.UWP\Assets\*.png" Link="Assets\%(FileName)%(Extension)" />
<Content Include="Fonts\winjs-symbols.woff2" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="WasmCSS\Fonts.css" />
<EmbeddedResource Include="WasmScripts\AppManifest.js" />
</ItemGroup>
<ItemGroup>
<LinkerDescriptor Include="LinkerConfig.xml" />
</ItemGroup>
<ItemGroup>
<!--
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<WasmHead>true</WasmHead>
<DefineConstants>$(DefineConstants);__WASM__</DefineConstants>
<NoWarn>NU1701</NoWarn>
<UnoUIUseRoslynSourceGenerators>true</UnoUIUseRoslynSourceGenerators>
</PropertyGroup>
<PropertyGroup>
<IsUiAutomationMappingEnabled>true</IsUiAutomationMappingEnabled>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<MonoRuntimeDebuggerEnabled>true</MonoRuntimeDebuggerEnabled>
<DefineConstants>$(DefineConstants);TRACE;DEBUG</DefineConstants>
<DebugType>portable</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<Content Include="..\Sample.UWP\Assets\*.png" Link="Assets\%(FileName)%(Extension)" />
<Content Include="Fonts\winjs-symbols.woff2" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="WasmCSS\Fonts.css" />
<EmbeddedResource Include="WasmScripts\AppManifest.js" />
</ItemGroup>
<ItemGroup>
<LinkerDescriptor Include="LinkerConfig.xml" />
</ItemGroup>
<ItemGroup>
<!--
This item group is required by the project templace because of the
new SDK-Style project, otherwise some files are not aded automatically.
You can safely remove this ItemGroup completely.
-->
<Compile Remove="Program.cs" />
<Compile Include="Program.cs" />
<Content Include="LinkerConfig.xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Uno.UI.WebAssembly" Version="3.7.6" />
<PackageReference Include="Uno.Wasm.Bootstrap" Version="7.0.20" />
<PackageReference Include="Uno.Wasm.Bootstrap.DevServer" Version="7.0.20" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="5.0.1" />
</ItemGroup>
<Import Project="..\Sample.Shared\Sample.Shared.projitems" Label="Shared" Condition="Exists('..\Sample.Shared\Sample.Shared.projitems')" />
<Compile Remove="Program.cs" />
<Compile Include="Program.cs" />
<Content Include="LinkerConfig.xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Uno.UI.WebAssembly" Version="3.7.6" />
<PackageReference Include="Uno.Wasm.Bootstrap" Version="7.0.20" />
<PackageReference Include="Uno.Wasm.Bootstrap.DevServer" Version="7.0.20" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="5.0.1" />
</ItemGroup>
<Import Project="..\Sample.Shared\Sample.Shared.projitems" Label="Shared" Condition="Exists('..\Sample.Shared\Sample.Shared.projitems')" />
</Project>

Просмотреть файл

@ -179,29 +179,38 @@ namespace Uno.UITests.Helpers
{
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(true);
}
else
{
configurator = configurator
.Headless(false)
.SeleniumArgument("--remote-debugging-port=9222");
}
else
{
configurator = configurator
.Headless(true);
}
_currentApp = configurator.ScreenShotsPath(TestContext.CurrentContext.TestDirectory)
_currentApp = configurator
.ScreenShotsPath(TestContext.CurrentContext.TestDirectory)
.StartApp();
return _currentApp;

Просмотреть файл

@ -45,9 +45,26 @@ namespace Uno.UITests.Helpers
/// </remarks>
public string ChromeDriverPath { get; set; }
/// <summary>
/// Defines the location of selenium driver.
/// </summary>
/// <remarks>
/// If not defined, the test engine will select the version based on
/// the currently installed browsers version.
/// </remarks>
public string SeleniumDriverPath { get; set; }
/// <summary>
/// Defines if the browser tests are running in chrome without a window.
/// </summary>
public bool WebAssemblyHeadless { get; set; } = true;
/// <summary>
/// Defines the browser to use for the Web platform. Cf. remarks about compatibility
/// </summary>
/// <remarks>
/// Note that all browser does not supports all options defined here. For instance Edge does support only the <see cref="SeleniumDriverPath"/>.
/// </remarks>
public Browser WebAssemblyBrowser { get; set; } = Browser.Chrome;
}
}

Просмотреть файл

@ -0,0 +1,18 @@
using System;
using System.Linq;
namespace Uno.UITests.Helpers
{
/// <summary>
/// Supported web browsers for UI tests
/// </summary>
public enum Browser
{
Chrome,
/// <summary>
/// The **CHROMIUM Based** Edge web browser
/// </summary>
Edge
}
}

Просмотреть файл

@ -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<string> InternalSeleniumArgument = new List<string>();
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; }
/// <summary>
/// This parameters allows to provide a set of additional parameters to be provided to the WebDriver.
/// </summary>
public ChromeAppConfigurator SeleniumArgument(string argument) { InternalSeleniumArgument.Add(argument); return this; }
/// <summary>
/// Enables the detection of the docker environment to configure the WebDriver accordingly. Enabled by default.
/// </summary>
public ChromeAppConfigurator DetectDockerEnvironment(bool enabled) { InternalDetectDockerEnvironment = enabled; return this; }
/// <summary>
/// Runs the browser as headless. Defaults to true.
/// </summary>
/// <param name="isHeadless"></param>
/// <returns></returns>
public ChromeAppConfigurator Headless(bool isHeadless) { InternalHeadless = isHeadless; return this; }
/// <summary>
/// Sets the window size. Defaults to 1024x768
/// </summary>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns></returns>
public ChromeAppConfigurator WindowSize(int width, int height)
{
InternalWindowWidth = width;
InternalWindowHeight = height;
return this;
}
public IApp StartApp() => new SeleniumApp(this);
}
}

Просмотреть файл

@ -6,7 +6,6 @@ namespace Uno.UITest.Selenium
{
public static class ConfigureApp
{
public static ChromeAppConfigurator WebAssembly =>
new ChromeAppConfigurator();
public static SeleniumAppConfigurator WebAssembly => new SeleniumAppConfigurator();
}
}

Просмотреть файл

@ -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; }
}
}

Просмотреть файл

@ -3,15 +3,12 @@ using System.Collections.Generic;
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 @@ namespace Uno.UITest.Selenium
.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<Actions> action)
{
@ -490,7 +294,6 @@ namespace Uno.UITest.Selenium
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 @@ namespace Uno.UITest.Selenium
throw new InvalidOperationException($"Invalid query results");
}
}
}

Просмотреть файл

@ -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<string> InternalSeleniumArgument = new List<string>();
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;
}
/// <summary>
/// This parameters allows to provide a set of additional parameters to be provided to the WebDriver.
/// </summary>
public SeleniumAppConfigurator SeleniumArgument(string argument)
{
InternalSeleniumArgument.Add(argument);
return this;
}
/// <summary>
/// Enables the detection of the docker environment to configure the WebDriver accordingly. Enabled by default.
/// </summary>
public SeleniumAppConfigurator DetectDockerEnvironment(bool enabled)
{
InternalDetectDockerEnvironment = enabled;
return this;
}
/// <summary>
/// Runs the browser as headless. Defaults to true.
/// </summary>
/// <param name="isHeadless"></param>
/// <returns></returns>
public SeleniumAppConfigurator Headless(bool isHeadless)
{
InternalHeadless = isHeadless;
return this;
}
/// <summary>
/// Sets the window size. Defaults to 1024x768
/// </summary>
/// <param name="width"></param>
/// <param name="height"></param>
/// <returns></returns>
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
}
}

Просмотреть файл

@ -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<Models.Root>(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; }
/// <summary>
/// Gets the file version of the provided browser path
/// </summary>
/// <param name="browserPath">Path to the browser executable</param>
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}].");
}
}
/// <summary>
/// Gets the target standard install path for the given version of this driver
/// </summary>
/// <param name="version">The version of the driver</param>
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");
}
/// <summary>
/// Download the zip package of the driver, and extract the driver executable to the target install path.
/// </summary>
/// <param name="driverSourceUri">The Uri of the package of the driver to download</param>
/// <param name="driverInstallPath">The target install path of the driver</param>
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.");
}
}
}
}

Просмотреть файл

@ -22,6 +22,7 @@
<ItemGroup>
<PackageReference Include="Selenium.WebDriver" Version="4.2.0" />
<PackageReference Include="System.Text.Json" Version="7.0.3" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -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