Skip to content

Commit cd2e97b

Browse files
authored
Merge pull request #50 from csf-dev/49-customize-options
Resolve #49 - add options customization and fix build on Linux
2 parents 36773e1 + cd46bc7 commit cd2e97b

10 files changed

Lines changed: 208 additions & 18 deletions

.appveyor.yml

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,40 @@
1-
image: Visual Studio 2022
1+
environment:
2+
matrix:
3+
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022
4+
JAVA_HOME: C:\Program Files\Java\jdk17
5+
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2204
6+
JAVA_HOME: /usr/lib/jvm/jdk15
7+
8+
skip_branch_with_pr: true
9+
10+
# A note/reminder for readers: Script items prefixed "cmd:" are executed on Windows-only environments.
11+
# Items with no prefix (or "ps:" prefix) are run on all environments (Windows & Linux)
12+
213
version: '{branch}-{build}'
14+
315
init:
4-
- cmd: git config --global core.autocrlf true
16+
- cmd: git config --global core.autocrlf true
17+
18+
install:
19+
# This was taken from https://stackoverflow.com/questions/60304251/unable-to-open-x-display-when-trying-to-run-google-chrome-on-centos-rhel-7-5
20+
# It's the minimum dependencies for running Chrome in a headless environment on Linux
21+
- sh: |
22+
sudo apt-get update
23+
sudo apt install -y xorg xvfb gtk2-engines-pixbuf dbus-x11 xfonts-base xfonts-100dpi xfonts-75dpi xfonts-cyrillic xfonts-scalable
24+
525
before_build:
6-
- dotnet tool update -g docfx
26+
- dotnet --version
27+
- dotnet restore --verbosity m
28+
- dotnet clean
29+
- cmd: dotnet tool update -g docfx
30+
# Activate Xvfb and export a display so that Chrome can run in Linux
31+
- sh: |
32+
Xvfb -ac :99 -screen 0 1280x1024x16 &
33+
export DISPLAY=:99
34+
735
build_script:
836
- dotnet build
9-
- docfx CSF.Extensions.WebDriver.Docs\docfx.json
37+
- cmd: docfx CSF.Extensions.WebDriver.Docs\docfx.json
38+
1039
test_script:
1140
- dotnet test

CSF.Extensions.WebDriver.Tests/CSF.Extensions.WebDriver.Tests.csproj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
2525
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
2626
<!-- Floating version should keep ChromeDriver up to date with the browser on the OS. See #47 for more info. -->
27-
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="*" />
27+
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="*" Condition="'$(CI_LINUX)' != 'true'" />
28+
<!-- AppVeyor Linux builds use a fixed Chrome version, this driver version must match it; note that
29+
this might change over time (an will need to be manually kept in-sync) as they upgrade their CI images -->
30+
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="127.0.6533.9900" Condition="'$(CI_LINUX)' == 'true'" />
2831
</ItemGroup>
2932

3033
<ItemGroup>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using OpenQA.Selenium.Chrome;
2+
3+
namespace CSF.Extensions.WebDriver.Factories;
4+
5+
public class SampleCustomizer : ICustomizesOptions<ChromeOptions>
6+
{
7+
public void CustomizeOptions(ChromeOptions options) { /* Intentional no-op */ }
8+
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,19 @@ public async Task ConfigureShouldBeAbleToGetSelectedConfigWhenThereIsOnlyOnePres
121121

122122
Assert.That(options.GetSelectedConfiguration()?.DriverType, Is.EqualTo("ChromeDriver"));
123123
}
124+
125+
[Test,AutoMoqData]
126+
public async Task ConfigureShouldBeAbleToAddACustomizerToSomeOptions([StandardTypes] IGetsWebDriverAndOptionsTypes typeProvider)
127+
{
128+
var options = await GetOptionsAsync(typeProvider,
129+
@"{
130+
""DriverConfigurations"": {
131+
""Test"": { ""DriverType"": ""ChromeDriver"", ""OptionsCustomizerType"": ""CSF.Extensions.WebDriver.Factories.SampleCustomizer, CSF.Extensions.WebDriver.Tests"" }
132+
}
133+
}");
134+
135+
Assert.That(options.GetSelectedConfiguration()?.OptionsCustomizer, Is.InstanceOf<SampleCustomizer>());
136+
}
124137

125138
[Test,AutoMoqData]
126139
public async Task ConfigureShouldBeAbleToGetSelectedConfigWhenASelectedConfigIsNamed([StandardTypes] IGetsWebDriverAndOptionsTypes typeProvider)

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,31 @@ public void GetWebDriverShouldCustomiseDriverOptionsWithCallbackWhenItIsSpecifie
3333
using var driver = sut.GetWebDriver(options, o => o.AddAdditionalOption("Foo", "Bar")).WebDriver;
3434
Assert.That(driverOptions.ToCapabilities()["Foo"], Is.EqualTo("Bar"));
3535
}
36+
37+
[Test,AutoMoqData]
38+
public void GetWebDriverShouldCustomiseDriverFromCustomizerInstanceIfSpecified([StandardTypes] IGetsWebDriverAndOptionsTypes typeProvider,
39+
WebDriverFromOptionsFactory sut)
40+
{
41+
var driverOptions = new ChromeOptions();
42+
var customizer = new AppveyorLinuxChromeCustomizer();
43+
var options = new WebDriverCreationOptions
44+
{
45+
DriverType = nameof(ChromeDriver),
46+
OptionsFactory = () => driverOptions,
47+
OptionsCustomizer = customizer,
48+
};
49+
50+
using var driver = sut.GetWebDriver(options);
51+
Assert.That(customizer.IsCustomized, Is.True);
52+
}
53+
54+
public class AppveyorLinuxChromeCustomizer : ICustomizesOptions<ChromeOptions>
55+
{
56+
public bool IsCustomized { get; private set; }
57+
58+
public void CustomizeOptions(ChromeOptions options)
59+
{
60+
IsCustomized = true;
61+
}
62+
}
3663
}

CSF.Extensions.WebDriver/CSF.Extensions.WebDriver.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<TargetFrameworks>netstandard2.0;net8.0;net462</TargetFrameworks>
55
<RootNamespace>CSF.Extensions.WebDriver</RootNamespace>
6-
<DocumentationFile>$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
6+
<DocumentationFile>$(MSBuildProjectDirectory)\bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
77
<VersionPrefix>2.0.0</VersionPrefix>
88
<PackageReadmeFile>README.md</PackageReadmeFile>
99
<PackageLicenseExpression>MIT</PackageLicenseExpression>
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, new[] { 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)