|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.Configuration;
using Microsoft.Playwright;
namespace Microsoft.AspNetCore.BrowserTesting;
public class BrowserManagerConfiguration
{
public BrowserManagerConfiguration(IConfiguration configuration)
{
Load(configuration);
}
public int TimeoutInMilliseconds { get; private set; }
public int TimeoutAfterFirstFailureInMilliseconds { get; private set; }
public string BaseArtifactsFolder { get; private set; }
public bool IsDisabled { get; private set; }
public BrowserTypeLaunchOptions GlobalBrowserOptions { get; private set; }
public BrowserNewContextOptions GlobalContextOptions { get; private set; }
public IDictionary<string, BrowserOptions> BrowserOptions { get; } =
new Dictionary<string, BrowserOptions>(StringComparer.OrdinalIgnoreCase);
public ISet<string> DisabledBrowsers { get; } =
new HashSet<string>(StringComparer.OrdinalIgnoreCase);
public IDictionary<string, BrowserNewContextOptions> ContextOptions { get; private set; } =
new Dictionary<string, BrowserNewContextOptions>(StringComparer.OrdinalIgnoreCase);
public BrowserTypeLaunchOptions GetBrowserTypeLaunchOptions(BrowserTypeLaunchOptions browserLaunchOptions)
{
if (browserLaunchOptions == null)
{
return GlobalBrowserOptions;
}
else
{
return Combine(GlobalBrowserOptions, browserLaunchOptions);
}
}
public BrowserNewContextOptions GetContextOptions(string browser)
{
if (!BrowserOptions.TryGetValue(browser, out var browserOptions))
{
throw new InvalidOperationException($"Browser '{browser}' is not configured.");
}
else if (browserOptions.DefaultContextOptions == null)
{
// Cheap clone
return Combine(GlobalContextOptions, null);
}
{
return Combine(GlobalContextOptions, browserOptions.DefaultContextOptions);
}
}
public BrowserNewContextOptions GetContextOptions(string browser, string contextName) =>
Combine(GetContextOptions(browser.ToString()), ContextOptions.TryGetValue(contextName, out var context) ? context : throw new InvalidOperationException("Invalid context name"));
public BrowserNewContextOptions GetContextOptions(string browser, string contextName, BrowserNewContextOptions options) =>
Combine(GetContextOptions(browser, contextName), options);
private void Load(IConfiguration configuration)
{
TimeoutInMilliseconds = configuration.GetValue(nameof(TimeoutInMilliseconds), 30000);
TimeoutAfterFirstFailureInMilliseconds = configuration.GetValue(nameof(TimeoutAfterFirstFailureInMilliseconds), 10000);
IsDisabled = configuration.GetValue(nameof(IsDisabled), false);
BaseArtifactsFolder = Path.GetFullPath(configuration.GetValue(nameof(BaseArtifactsFolder), Path.Combine(Directory.GetCurrentDirectory(), "playwright")));
Directory.CreateDirectory(BaseArtifactsFolder);
var defaultBrowserOptions = configuration.GetSection(nameof(GlobalBrowserOptions));
if (defaultBrowserOptions.Exists())
{
GlobalBrowserOptions = LoadBrowserLaunchOptions(defaultBrowserOptions);
}
var defaultContextOptions = configuration.GetSection(nameof(GlobalContextOptions));
if (defaultContextOptions.Exists())
{
GlobalContextOptions = LoadContextOptions(configuration.GetSection(nameof(GlobalContextOptions)));
}
var browsersOptions = configuration.GetSection(nameof(BrowserOptions));
if (!browsersOptions.Exists())
{
throw new InvalidOperationException("Browsers not configured.");
}
foreach (var browser in browsersOptions.GetChildren())
{
var browserName = browser.Key;
var isEnabled = browser.GetValue<bool>("IsEnabled");
var browserKind = browser.GetValue<BrowserKind>("BrowserKind");
if (!isEnabled)
{
DisabledBrowsers.Add(browserName);
continue;
}
var defaultContextOptionsSection = browser.GetSection("DefaultContextOptions");
var browserOptions = new BrowserOptions(
browserKind,
LoadBrowserLaunchOptions(browser),
defaultContextOptionsSection.Exists() ? LoadContextOptions(defaultContextOptionsSection) : null);
BrowserOptions.Add(browserName, browserOptions);
}
var contextOptions = configuration.GetSection("ContextOptions");
foreach (var option in contextOptions.GetChildren())
{
ContextOptions.Add(option.Key, LoadContextOptions(option));
}
}
private BrowserNewContextOptions LoadContextOptions(IConfiguration configuration) => EnsureFoldersExist(new BrowserNewContextOptions
{
Proxy = BindValue<Proxy>(configuration, nameof(BrowserNewContextOptions.Proxy)),
RecordVideoDir = configuration.GetValue<string>(nameof(BrowserNewContextOptions.RecordVideoDir)),
RecordVideoSize = BindValue<RecordVideoSize>(configuration, nameof(BrowserNewContextOptions.RecordVideoSize)),
RecordHarPath = configuration.GetValue<string>(nameof(BrowserNewContextOptions.RecordHarPath)),
RecordHarOmitContent = configuration.GetValue<bool?>(nameof(BrowserNewContextOptions.RecordHarOmitContent)),
ExtraHTTPHeaders = BindMultiValueMap(
configuration.GetSection(nameof(BrowserNewContextOptions.ExtraHTTPHeaders)),
argsMap => argsMap.ToDictionary(kvp => kvp.Key, kvp => string.Join(", ", kvp.Value))),
Locale = configuration.GetValue<string>(nameof(BrowserNewContextOptions.Locale)),
ColorScheme = configuration.GetValue<ColorScheme?>(nameof(BrowserNewContextOptions.ColorScheme)),
AcceptDownloads = configuration.GetValue<bool?>(nameof(BrowserNewContextOptions.AcceptDownloads)),
HasTouch = configuration.GetValue<bool?>(nameof(BrowserNewContextOptions.HasTouch)),
HttpCredentials = configuration.GetValue<HttpCredentials>(nameof(BrowserNewContextOptions.HttpCredentials)),
DeviceScaleFactor = configuration.GetValue<float?>(nameof(BrowserNewContextOptions.DeviceScaleFactor)),
Offline = configuration.GetValue<bool?>(nameof(BrowserNewContextOptions.Offline)),
IsMobile = configuration.GetValue<bool?>(nameof(BrowserNewContextOptions.IsMobile)),
// TODO: Map this properly
Permissions = configuration.GetValue<IEnumerable<string>>(nameof(BrowserNewContextOptions.Permissions)),
Geolocation = BindValue<Geolocation>(configuration, nameof(BrowserNewContextOptions.Geolocation)),
TimezoneId = configuration.GetValue<string>(nameof(BrowserNewContextOptions.TimezoneId)),
IgnoreHTTPSErrors = configuration.GetValue<bool?>(nameof(BrowserNewContextOptions.IgnoreHTTPSErrors)),
JavaScriptEnabled = configuration.GetValue<bool?>(nameof(BrowserNewContextOptions.JavaScriptEnabled)),
BypassCSP = configuration.GetValue<bool?>(nameof(BrowserNewContextOptions.BypassCSP)),
UserAgent = configuration.GetValue<string>(nameof(BrowserNewContextOptions.UserAgent)),
ViewportSize = BindValue<ViewportSize>(configuration, nameof(BrowserNewContextOptions.ViewportSize)),
StorageStatePath = configuration.GetValue<string>(nameof(BrowserNewContextOptions.StorageStatePath)),
StorageState = configuration.GetValue<string>(nameof(BrowserNewContextOptions.StorageState))
});
private static T BindValue<T>(IConfiguration configuration, string key) where T : new()
{
var instance = new T();
var section = configuration.GetSection(key);
configuration.Bind(key, instance);
return section.Exists() ? instance : default;
}
private BrowserNewContextOptions EnsureFoldersExist(BrowserNewContextOptions browserContextOptions)
{
if (browserContextOptions?.RecordVideoDir != null)
{
browserContextOptions.RecordVideoDir = EnsureFolderExists(browserContextOptions.RecordVideoDir);
}
if (browserContextOptions?.RecordHarPath != null)
{
browserContextOptions.RecordHarPath = EnsureFolderExists(browserContextOptions.RecordHarPath);
}
return browserContextOptions;
string EnsureFolderExists(string folderPath)
{
if (Path.IsPathRooted(folderPath))
{
Directory.CreateDirectory(folderPath);
return folderPath;
}
else
{
folderPath = Path.Combine(BaseArtifactsFolder, folderPath);
Directory.CreateDirectory(folderPath);
return folderPath;
}
}
}
private static BrowserTypeLaunchOptions LoadBrowserLaunchOptions(IConfiguration configuration) => new BrowserTypeLaunchOptions
{
IgnoreDefaultArgs = BindArgumentMap(configuration.GetSection(nameof(BrowserTypeLaunchOptions.IgnoreAllDefaultArgs))),
ChromiumSandbox = configuration.GetValue<bool?>(nameof(BrowserTypeLaunchOptions.ChromiumSandbox)),
HandleSIGHUP = configuration.GetValue<bool?>(nameof(BrowserTypeLaunchOptions.HandleSIGHUP)),
HandleSIGTERM = configuration.GetValue<bool?>(nameof(BrowserTypeLaunchOptions.HandleSIGTERM)),
HandleSIGINT = configuration.GetValue<bool?>(nameof(BrowserTypeLaunchOptions.HandleSIGINT)),
IgnoreAllDefaultArgs = configuration.GetValue<bool?>(nameof(BrowserTypeLaunchOptions.IgnoreAllDefaultArgs)),
SlowMo = configuration.GetValue<int?>(nameof(BrowserTypeLaunchOptions.SlowMo)),
Env = configuration.GetValue<Dictionary<string, string>>(nameof(BrowserTypeLaunchOptions.Env)),
DownloadsPath = configuration.GetValue<string>(nameof(BrowserTypeLaunchOptions.DownloadsPath)),
ExecutablePath = configuration.GetValue<string>(nameof(BrowserTypeLaunchOptions.ExecutablePath)),
Args = BindMultiValueMap(
configuration.GetSection(nameof(BrowserTypeLaunchOptions.Args)),
argsMap => argsMap.SelectMany(argNameValue => argNameValue.Value.Prepend(argNameValue.Key)).ToArray()),
Headless = configuration.GetValue<bool?>(nameof(BrowserTypeLaunchOptions.Headless)),
Timeout = configuration.GetValue<int?>(nameof(BrowserTypeLaunchOptions.Timeout)),
Proxy = configuration.GetValue<Proxy>(nameof(BrowserTypeLaunchOptions.Proxy))
};
private static T BindMultiValueMap<T>(IConfigurationSection processArgsMap, Func<Dictionary<string, HashSet<string>>, T> mapper)
{
// TODO: We need a way to pass in arguments that allows overriding values through our config system.
// "Args": {
// // switch argument
// "arg": true,
// // single value argument
// "arg2": "value",
// // remove single value argument
// "arg3": null,
// // multi-value argument
// "arg4": {
// "value": true,
// "otherValue": "false"
// }
if (!processArgsMap.Exists())
{
return mapper(new Dictionary<string, HashSet<string>>());
}
var argsMap = new Dictionary<string, HashSet<string>>();
foreach (var arg in processArgsMap.GetChildren())
{
var argName = arg.Key;
// Its a single value being removed
if (arg.Value == null)
{
argsMap.Remove(argName);
}
else if (arg.GetChildren().Count() > 1)
{
// Its an object mapping multiple values in the form "--arg value1 value2 value3"
var argValues = InitializeMapValue(argsMap, argName);
foreach (var (value, enabled) in arg.Get<Dictionary<string, bool>>())
{
if (enabled)
{
argValues.Add(value);
}
else
{
argValues.Remove(value);
}
}
}
else if (!bool.TryParse(arg.Value, out var switchValue))
{
// Its a single value
var argValue = InitializeMapValue(argsMap, argName);
argValue.Clear();
argValue.Add(arg.Value);
}
else
{
// Its a switch value
if (switchValue)
{
_ = InitializeMapValue(argsMap, argName);
}
else
{
argsMap.Remove(argName);
}
}
}
return mapper(argsMap);
static HashSet<string> InitializeMapValue(Dictionary<string, HashSet<string>> argsMap, string argName)
{
if (!argsMap.TryGetValue(argName, out var argValue))
{
argValue = new HashSet<string>();
argsMap[argName] = argValue;
}
return argValue;
}
}
private static string[] BindArgumentMap(IConfigurationSection configuration) => configuration.Exists() switch
{
false => Array.Empty<string>(),
true => configuration.Get<Dictionary<string, bool>>().Where(kvp => kvp.Value == true).Select(kvp => kvp.Key).ToArray()
};
private static BrowserNewContextOptions Combine(BrowserNewContextOptions defaultOptions, BrowserNewContextOptions overrideOptions) =>
new()
{
Proxy = overrideOptions?.Proxy != default ? overrideOptions.Proxy : defaultOptions.Proxy,
RecordVideoDir = overrideOptions?.RecordVideoDir != default ? overrideOptions.RecordVideoDir : defaultOptions.RecordVideoDir,
RecordVideoSize = overrideOptions?.RecordVideoSize != default ? overrideOptions.RecordVideoSize : defaultOptions.RecordVideoSize,
RecordHarPath = overrideOptions?.RecordHarPath != default ? overrideOptions.RecordHarPath : defaultOptions.RecordHarPath,
RecordHarOmitContent = overrideOptions?.RecordHarOmitContent != default ? overrideOptions.RecordHarOmitContent : defaultOptions.RecordHarOmitContent,
ExtraHTTPHeaders = overrideOptions?.ExtraHTTPHeaders != default ? overrideOptions.ExtraHTTPHeaders : defaultOptions.ExtraHTTPHeaders,
Locale = overrideOptions?.Locale != default ? overrideOptions.Locale : defaultOptions.Locale,
ColorScheme = overrideOptions?.ColorScheme != default ? overrideOptions.ColorScheme : defaultOptions.ColorScheme,
AcceptDownloads = overrideOptions?.AcceptDownloads != default ? overrideOptions.AcceptDownloads : defaultOptions.AcceptDownloads,
HasTouch = overrideOptions?.HasTouch != default ? overrideOptions.HasTouch : defaultOptions.HasTouch,
HttpCredentials = overrideOptions?.HttpCredentials != default ? overrideOptions.HttpCredentials : defaultOptions.HttpCredentials,
DeviceScaleFactor = overrideOptions?.DeviceScaleFactor != default ? overrideOptions.DeviceScaleFactor : defaultOptions.DeviceScaleFactor,
Offline = overrideOptions?.Offline != default ? overrideOptions.Offline : defaultOptions.Offline,
IsMobile = overrideOptions?.IsMobile != default ? overrideOptions.IsMobile : defaultOptions.IsMobile,
Permissions = overrideOptions?.Permissions != default ? overrideOptions.Permissions : defaultOptions.Permissions,
Geolocation = overrideOptions?.Geolocation != default ? overrideOptions.Geolocation : defaultOptions.Geolocation,
TimezoneId = overrideOptions?.TimezoneId != default ? overrideOptions.TimezoneId : defaultOptions.TimezoneId,
IgnoreHTTPSErrors = overrideOptions?.IgnoreHTTPSErrors != default ? overrideOptions.IgnoreHTTPSErrors : defaultOptions.IgnoreHTTPSErrors,
JavaScriptEnabled = overrideOptions?.JavaScriptEnabled != default ? overrideOptions.JavaScriptEnabled : defaultOptions.JavaScriptEnabled,
BypassCSP = overrideOptions?.BypassCSP != default ? overrideOptions.BypassCSP : defaultOptions.BypassCSP,
UserAgent = overrideOptions?.UserAgent != default ? overrideOptions.UserAgent : defaultOptions.UserAgent,
ViewportSize = overrideOptions?.ViewportSize != default ? overrideOptions.ViewportSize : defaultOptions.ViewportSize,
StorageStatePath = overrideOptions?.StorageStatePath != default ? overrideOptions.StorageStatePath : defaultOptions.StorageStatePath,
StorageState = overrideOptions?.StorageState != default ? overrideOptions.StorageState : defaultOptions.StorageState
};
private static BrowserTypeLaunchOptions Combine(BrowserTypeLaunchOptions defaultOptions, BrowserTypeLaunchOptions overrideOptions) =>
new()
{
IgnoreDefaultArgs = overrideOptions.IgnoreDefaultArgs != default ? overrideOptions.IgnoreDefaultArgs : defaultOptions.IgnoreDefaultArgs,
ChromiumSandbox = overrideOptions.ChromiumSandbox != default ? overrideOptions.ChromiumSandbox : defaultOptions.ChromiumSandbox,
HandleSIGHUP = overrideOptions.HandleSIGHUP != default ? overrideOptions.HandleSIGHUP : defaultOptions.HandleSIGHUP,
HandleSIGTERM = overrideOptions.HandleSIGTERM != default ? overrideOptions.HandleSIGTERM : defaultOptions.HandleSIGTERM,
HandleSIGINT = overrideOptions.HandleSIGINT != default ? overrideOptions.HandleSIGINT : defaultOptions.HandleSIGINT,
IgnoreAllDefaultArgs = overrideOptions.IgnoreAllDefaultArgs != default ? overrideOptions.IgnoreAllDefaultArgs : defaultOptions.IgnoreAllDefaultArgs,
SlowMo = overrideOptions.SlowMo != default ? overrideOptions.SlowMo : defaultOptions.SlowMo,
Env = overrideOptions.Env != default ? overrideOptions.Env : defaultOptions.Env,
DownloadsPath = overrideOptions.DownloadsPath != default ? overrideOptions.DownloadsPath : defaultOptions.DownloadsPath,
ExecutablePath = overrideOptions.ExecutablePath != default ? overrideOptions.ExecutablePath : defaultOptions.ExecutablePath,
Args = overrideOptions.Args != default ? overrideOptions.Args : defaultOptions.Args,
Headless = overrideOptions.Headless != default ? overrideOptions.Headless : defaultOptions.Headless,
Timeout = overrideOptions.Timeout != default ? overrideOptions.Timeout : defaultOptions.Timeout,
Proxy = overrideOptions.Proxy != default ? overrideOptions.Proxy : defaultOptions.Proxy
};
}
public sealed class BrowserOptions
{
public BrowserKind BrowserKind { get; }
public BrowserTypeLaunchOptions BrowserLaunchOptions { get; }
public BrowserNewContextOptions DefaultContextOptions { get; }
public BrowserOptions(BrowserKind browserKind, BrowserTypeLaunchOptions browserLaunchOptions, BrowserNewContextOptions defaultContextOptions)
{
BrowserKind = browserKind;
BrowserLaunchOptions = browserLaunchOptions;
DefaultContextOptions = defaultContextOptions;
}
}
|