Skip to content

Commit 7f17228

Browse files
committed
WIP #49 - Add customizer to test
1 parent dae71e4 commit 7f17228

5 files changed

Lines changed: 136 additions & 12 deletions

File tree

CSF.Extensions.WebDriver.Tests/Factories/WebDriverFromOptionsFactoryTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public void GetWebDriverShouldCreateALocalChromeDriverFromAppropriateOptions([St
1313
{
1414
DriverType = nameof(ChromeDriver),
1515
OptionsFactory = () => new ChromeOptions(),
16+
OptionsCustomizer = new AppveyorLinuxChromeCustomizer(),
1617
};
1718

1819
using var driver = sut.GetWebDriver(options).WebDriver;
@@ -28,9 +29,22 @@ public void GetWebDriverShouldCustomiseDriverOptionsWithCallbackWhenItIsSpecifie
2829
{
2930
DriverType = nameof(ChromeDriver),
3031
OptionsFactory = () => driverOptions,
32+
OptionsCustomizer = new AppveyorLinuxChromeCustomizer(),
3133
};
3234

3335
using var driver = sut.GetWebDriver(options, o => o.AddAdditionalOption("Foo", "Bar")).WebDriver;
3436
Assert.That(driverOptions.ToCapabilities()["Foo"], Is.EqualTo("Bar"));
3537
}
38+
39+
public class AppveyorLinuxChromeCustomizer : ICustomizesOptions<ChromeOptions>
40+
{
41+
public void CustomizeOptions(ChromeOptions options)
42+
{
43+
if(string.Equals(Environment.GetEnvironmentVariable("APPVEYOR"), bool.TrueString, StringComparison.InvariantCultureIgnoreCase)
44+
&& Environment.GetEnvironmentVariable("APPVEYOR_BUILD_WORKER_IMAGE")!.Contains("ubuntu", StringComparison.InvariantCultureIgnoreCase))
45+
{
46+
options.AddArgument("--no-sandbox");
47+
}
48+
}
49+
}
3650
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using OpenQA.Selenium;
2+
3+
namespace CSF.Extensions.WebDriver.Factories
4+
{
5+
/// <summary>
6+
/// An object which can customize the options for a web driver before they are used to create the <see cref="IWebDriver"/>.
7+
/// </summary>
8+
/// <remarks>
9+
/// <para>
10+
/// Implementations of this interface are rarely required. They are used to customize the options for creating a web driver
11+
/// after <see cref="WebDriverCreationOptions.OptionsFactory"/> has created the options instance but before it is used to
12+
/// create the web driver.
13+
/// </para>
14+
/// <para>
15+
/// This is useful when you need to customize the options for a web driver in a way which is not supported by the binding from
16+
/// a configuration file. For example, some web driver options do not provide property getters/setters but must be configured
17+
/// using methods. In this case you can implement this interface with a class to customize the options as required.
18+
/// </para>
19+
/// <para>
20+
/// The implementation of this interface should be specified via the <see cref="WebDriverCreationOptions.OptionsCustomizer"/>
21+
/// property. This will instantiate the customizer and call the <see cref="CustomizeOptions"/> method before the web driver is
22+
/// created.
23+
/// </para>
24+
/// </remarks>
25+
public interface ICustomizesOptions<in TOptions> where TOptions : DriverOptions
26+
{
27+
/// <summary>
28+
/// Customizes the options for a web driver.
29+
/// </summary>
30+
/// <param name="options">The WebDriver options.</param>
31+
void CustomizeOptions(TOptions options);
32+
}
33+
}

CSF.Extensions.WebDriver/Factories/WebDriverCreationConfigureOptions.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,21 @@ WebDriverCreationOptions GetDriverConfiguration(IConfigurationSection configurat
123123
return null;
124124
}
125125

126+
var customizerTypeName = configuration.GetValue<string>("OptionsCustomizerType");
127+
try
128+
{
129+
creationOptions.OptionsCustomizer = GetOptionsCustomizer(optionsType, customizerTypeName);
130+
}
131+
catch(Exception e)
132+
{
133+
logger.LogError(e,
134+
"An unexpected error occurred binding the {OptionsCustomizer} type {CustomizerType}; the configuration '{ConfigKey}' will be omitted.",
135+
nameof(WebDriverCreationOptions.OptionsCustomizer),
136+
customizerTypeName,
137+
configuration.Key);
138+
return null;
139+
}
140+
126141
return creationOptions;
127142
}
128143

@@ -136,6 +151,19 @@ static Func<DriverOptions> GetOptions(Type optionsType, IConfigurationSection co
136151
};
137152
}
138153

154+
static object GetOptionsCustomizer(Type optionsType, string customizerTypeName)
155+
{
156+
if(string.IsNullOrWhiteSpace(customizerTypeName)) return null;
157+
var customizerType = Type.GetType(customizerTypeName, true);
158+
159+
if(!typeof(ICustomizesOptions<>).MakeGenericType(optionsType).IsAssignableFrom(customizerType))
160+
throw new ArgumentException($"The specified customizer type must implement {nameof(ICustomizesOptions<DriverOptions>)}<{optionsType.Name}>.", nameof(customizerTypeName));
161+
if(customizerType.GetConstructor(Type.EmptyTypes) == null)
162+
throw new ArgumentException($"The specified customizer type must have a public parameterless constructor.", nameof(customizerTypeName));
163+
164+
return Activator.CreateInstance(customizerType);
165+
}
166+
139167
/// <summary>
140168
/// Initialises a new instance of <see cref="WebDriverCreationConfigureOptions"/>.
141169
/// </summary>

CSF.Extensions.WebDriver/Factories/WebDriverCreationOptions.cs

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -94,29 +94,57 @@ public class WebDriverCreationOptions
9494
public string GridUrl { get; set; }
9595

9696
/// <summary>
97-
/// Gets or sets a factory object which can create instances of WebDriver options, to be provided to the WebDriver implementation.
97+
/// Gets or sets a function which creates the object which derives from <see cref="DriverOptions"/>, used as the creation options for the <see cref="IWebDriver"/>.
9898
/// </summary>
9999
/// <remarks>
100100
/// <para>
101+
/// In the most common scenario - providing WebDriver options from a JSON configuration file such as <c>appsettings.json</c> - this property is bound
102+
/// from a configuration key named <c>Options</c>, rather than "OptionsFactory". In a configuration file, the options are specified as a simple
103+
/// JSON object. However, after binding to this property this becomes a factory function instead. That is because of two factors:
104+
/// </para>
105+
/// <list type="bullet">
106+
/// <item><description>Instances of types which derive from <see cref="DriverOptions"/> are not reusable and should not be shared between WebDriver instances</description></item>
107+
/// <item><description>This WebDriver factory framework must be capable of creating multiple <see cref="IWebDriver"/> instances from one configuration, thus
108+
/// requiring many options instances</description></item>
109+
/// When bound from a configuration file, the options object which would be returned from this factory function will have properties set
110+
/// as specified in that configuration.
111+
/// </list>
112+
/// <para>
101113
/// The return value of this function must be an object of an appropriate type to match the implementation of <see cref="IWebDriver"/>
102114
/// that is selected, via <see cref="DriverType"/>.
103-
/// When deserializing this value from configuration (such as an <c>appsettings.json</c> file), the <see cref="OptionsType"/>
104-
/// will be used to select the appropriate polymorphic type to which the configuration should be bound.
105-
/// For local WebDriver implementations which are shipped with Selenium, the options type need not be specified explicitly;
106-
/// it will be inferred from the chosen driver type.
115+
/// If this value was bound from a configuration file then the generated factory function will automatically instantiate an instance of either:
107116
/// </para>
117+
/// <list type="bullet">
118+
/// <item><description>The options type specified in the configuration file, if <see cref="OptionsType"/> is set</description></item>
119+
/// <item><description>The options type which is inferred from the <see cref="DriverType"/>, if <see cref="OptionsType"/> is not set.
120+
/// See the documentation for <see cref="OptionsType"/> for more information</description></item>
121+
/// </list>
122+
/// </remarks>
123+
public Func<DriverOptions> OptionsFactory { get; set; }
124+
125+
/// <summary>
126+
/// An optional object which implements <see cref="ICustomizesOptions{TOptions}"/> for the corresponding <see cref="DriverOptions"/>
127+
/// type for the <see cref="DriverType"/>/<see cref="OptionsType"/>.
128+
/// </summary>
129+
/// <remarks>
108130
/// <para>
109-
/// This option is provided as a factory, rather than an instance of <see cref="DriverOptions"/> because it will create options
110-
/// instances for - potentially - many usages throughout the application/test lifetime. It is a poor design choice to use a single
111-
/// options instance for every one of the WebDriver instances which will be used. In some cases, this would cause functional issues,
112-
/// such as where additional per-scenario capabilities need to be injected into each options instance.
131+
/// If this instance is bound from a configuration file - such as <c>appsettings.json</c> - then this property is bound from a configuration key named
132+
/// <c>OptionsCustomizerType</c> rather than "OptionsCustomizer". The value of that configuration key should be the assembly-qualified type name of
133+
/// the concrete implementation of <see cref="ICustomizesOptions{TOptions}"/> which should be used to customize the options. In this scenario this type
134+
/// must also have a public parameterless constructor.
113135
/// </para>
114136
/// <para>
115-
/// Please note that when using Microsoft.Extensions.Configuration to bind an instance of this type, this factory function is bound
116-
/// from an <see cref="IConfiguration"/> key named <c>Options</c> and not OptionsFactory as its member name might suggest.
137+
/// This configuration property is rarely required. This object is used to customize the options for creating a web driver
138+
/// after <see cref="OptionsFactory"/> has created the options instance but before it is used to
139+
/// create the web driver.
140+
/// </para>
141+
/// <para>
142+
/// This is useful when you need to customize the options for a web driver in a way which is not supported by the binding from
143+
/// a configuration file. For example, some web driver options do not provide property getters/setters but must be configured
144+
/// using methods. In this case you can implement this interface with a class to customize the options as required.
117145
/// </para>
118146
/// </remarks>
119-
public Func<DriverOptions> OptionsFactory { get; set; }
147+
public object OptionsCustomizer { get; set; }
120148

121149
/// <summary>
122150
/// Unneeded except in unusual circumstances, gets or sets the name of a type which is used to construct the WebDriver instance.

CSF.Extensions.WebDriver/Factories/WebDriverFromOptionsFactory.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Linq;
3+
using System.Reflection;
34
using Microsoft.Extensions.Logging;
45
using OpenQA.Selenium;
56

@@ -10,6 +11,8 @@ namespace CSF.Extensions.WebDriver.Factories
1011
/// </summary>
1112
public class WebDriverFromOptionsFactory : ICreatesWebDriverFromOptions
1213
{
14+
static readonly MethodInfo customizeGenericMethod = typeof(WebDriverFromOptionsFactory).GetMethod(nameof(CuztomizeOptionsGeneric), BindingFlags.NonPublic | BindingFlags.Static);
15+
1316
readonly IGetsWebDriverAndOptionsTypes typeProvider;
1417
readonly ILogger logger;
1518

@@ -28,12 +31,30 @@ public WebDriverAndOptions GetWebDriver(WebDriverCreationOptions options, Action
2831
nameof(options));
2932

3033
var driverOptions = options.OptionsFactory();
34+
CustomizeOptions(driverOptions, options.OptionsCustomizer);
3135
supplementaryConfiguration?.Invoke(driverOptions);
3236
var driver = (IWebDriver) Activator.CreateInstance(driverType, driverOptions);
3337
logger.LogInformation("Driver created via reflection: {Driver}", driver);
3438
return new WebDriverAndOptions(driver, driverOptions);
3539
}
3640

41+
static void CustomizeOptions(DriverOptions options, object customizer)
42+
{
43+
if (customizer is null) return;
44+
var method = customizeGenericMethod.MakeGenericMethod(options.GetType());
45+
method.Invoke(null, [options, customizer]);
46+
}
47+
48+
static void CuztomizeOptionsGeneric<TOptions>(TOptions options, object customizer)
49+
where TOptions : DriverOptions
50+
{
51+
if (!(customizer is ICustomizesOptions<TOptions> customizerInstance))
52+
throw new ArgumentException($"The customizer type {customizer.GetType().FullName} does not implement {customizeGenericMethod.Name}<{typeof(TOptions).Name}>.",
53+
nameof(customizer));
54+
55+
customizerInstance.CustomizeOptions(options);
56+
}
57+
3758
/// <summary>
3859
/// Initialises a new instance of <see cref="WebDriverFromOptionsFactory"/>.
3960
/// </summary>

0 commit comments

Comments
 (0)