Skip to content

Commit 97ebc5a

Browse files
committed
Resolve #51 - No longer madatory
This resolves the behaviour of the web driver factory so that unused config properties are not mandatory when using a custom factory type. * Also switches to a higher Selenium version for testing, which includes Selenium Manager.
1 parent 6dbc947 commit 97ebc5a

10 files changed

Lines changed: 306 additions & 145 deletions

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,15 @@
2323
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.0" />
2424
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
2525
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
26-
<!-- 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="*" 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'" />
26+
<PackageReference Include="Selenium.WebDriver" Version="4.15.0" />
3127
</ItemGroup>
3228

3329
<ItemGroup>
3430
<ProjectReference Include="..\CSF.Extensions.WebDriver\CSF.Extensions.WebDriver.csproj" />
3531
</ItemGroup>
3632

3733
<ItemGroup>
38-
<Content Include="appsettings.WebDriverFactoryIntegrationTests.json">
34+
<Content Include="appsettings.*.json">
3935
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
4036
</Content>
4137
</ItemGroup>

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,9 @@ static async Task<WebDriverCreationOptionsCollection> GetOptionsAsync(IGetsWebDr
240240
{
241241
var options = new WebDriverCreationOptionsCollection();
242242
var config = await GetConfigurationAsync(json);
243-
var sut = new WebDriverCreationConfigureOptions(typeProvider, config, logger ?? Mock.Of<ILogger<WebDriverCreationConfigureOptions>>());
243+
var sut = new WebDriverCreationConfigureOptions(new WebDriverConfigurationItemParser(typeProvider, Mock.Of<ILogger<WebDriverConfigurationItemParser>>()),
244+
config,
245+
logger ?? Mock.Of<ILogger<WebDriverCreationConfigureOptions>>());
244246
sut.Configure(options);
245247
return options;
246248
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ public void GetDefaultWebDriverShouldReturnADriverProxyWithIdentification()
3232
Assert.That(() => driver.WebDriver.GetBrowserId(), Is.Not.Null);
3333
}
3434

35+
[Test]
36+
public void DriverTypeNorOptionsTypeShouldBeMandatoryIfACustomFactoryTypeIsSpecified()
37+
{
38+
var services = GetServiceProvider(o => o.SelectedConfiguration = "OmittedDriverAndOptionsType");
39+
var driverFactory = services.GetRequiredService<IGetsWebDriver>();
40+
using var driver = driverFactory.GetDefaultWebDriver();
41+
Assert.That(() => driver.WebDriver.GetBrowserId(), Is.Not.Null);
42+
}
43+
3544
IServiceProvider GetServiceProvider(Action<WebDriverCreationOptionsCollection>? configureOptions = null)
3645
{
3746
var services = new ServiceCollection();

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,6 @@ public class FakeWebDriver : IWebDriver
160160

161161
public class FakeOptions : DriverOptions
162162
{
163-
[Obsolete("Obsolete because base class is obsolete")]
164-
public override void AddAdditionalCapability(string capabilityName, object capabilityValue) => throw new NotImplementedException();
165-
166163
public override ICapabilities ToCapabilities() => throw new NotImplementedException();
167164
}
168165
}

CSF.Extensions.WebDriver.Tests/appsettings.WebDriverFactoryIntegrationTests.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"__Description__": "This configuration file is used by WebDriverFactoryIntegrationTests. It describes RemoteWebDriver configs which will raise a crash errors because of unsupported URL schemes. They are useful because they prove that Selenium's functionality is being exercised.",
2+
"__Description__": "This configuration file is used by WebDriverFactoryIntegrationTests. It describes a number of WebDriver config scenarios which will be tested.",
33
"WebDriverFactory": {
44
"DriverConfigurations": {
55
"DefaultFake": {
@@ -17,6 +17,9 @@
1717
"DriverType": "RemoteWebDriver",
1818
"OptionsType": "ChromeOptions",
1919
"GridUrl": "nonsense://127.0.0.1/no-http-request/should-be-made"
20+
},
21+
"OmittedDriverAndOptionsType": {
22+
"DriverFactoryType": "CSF.Extensions.WebDriver.Factories.WebDriverFactoryIntegrationTests+FakeWebDriverFactory, CSF.Extensions.WebDriver.Tests"
2023
}
2124
},
2225
"SelectedConfiguration": "DefaultInvalid"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using Microsoft.Extensions.Configuration;
3+
4+
namespace CSF.Extensions.WebDriver.Factories
5+
{
6+
/// <summary>
7+
/// A service which reads an <see cref="IConfigurationSection"/> which describes a creation-strategy for a WebDriver, and gets an
8+
/// instance of <see cref="WebDriverCreationOptions"/>.
9+
/// </summary>
10+
public interface IParsesSingleWebDriverConfigurationSection
11+
{
12+
/// <summary>
13+
/// Gets an instance of <see cref="WebDriverCreationOptions"/> from the specified <see cref="IConfigurationSection"/>.
14+
/// </summary>
15+
/// <remarks>
16+
/// <para>
17+
/// Note that if the configuration is invalid, then this method will return a <see langword="null"/> instance of
18+
/// <see cref="WebDriverCreationOptions"/>.
19+
/// </para>
20+
/// </remarks>
21+
/// <param name="configuration">The configuration section which describes the configuration of a WebDriver.</param>
22+
/// <returns>A strongly-typed options object, or a <see langword="null"/> reference indicating an invalid configuration.</returns>
23+
/// <exception cref="ArgumentNullException">If <paramref name="configuration"/> is <see langword="null"/>.</exception>
24+
WebDriverCreationOptions GetDriverConfiguration(IConfigurationSection configuration);
25+
}
26+
}
27+
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
using System;
2+
using Microsoft.Extensions.Configuration;
3+
using Microsoft.Extensions.Logging;
4+
using OpenQA.Selenium;
5+
6+
namespace CSF.Extensions.WebDriver.Factories
7+
{
8+
/// <summary>
9+
/// Default implementation of <see cref="IParsesSingleWebDriverConfigurationSection"/>.
10+
/// </summary>
11+
public class WebDriverConfigurationItemParser : IParsesSingleWebDriverConfigurationSection
12+
{
13+
readonly IGetsWebDriverAndOptionsTypes typeProvider;
14+
readonly ILogger<WebDriverConfigurationItemParser> logger;
15+
16+
/// <inheritdoc/>
17+
public WebDriverCreationOptions GetDriverConfiguration(IConfigurationSection configuration)
18+
{
19+
if(configuration is null) throw new ArgumentNullException(nameof(configuration));
20+
21+
var creationOptions = new WebDriverCreationOptions
22+
{
23+
DriverType = configuration.GetValue<string>(nameof(WebDriverCreationOptions.DriverType)),
24+
OptionsType = configuration.GetValue<string>(nameof(WebDriverCreationOptions.OptionsType)),
25+
GridUrl = configuration.GetValue<string>(nameof(WebDriverCreationOptions.GridUrl)),
26+
DriverFactoryType = configuration.GetValue<string>(nameof(WebDriverCreationOptions.DriverFactoryType)),
27+
};
28+
29+
if(!TryGetDriverType(creationOptions, configuration, out var driverType))
30+
return null;
31+
32+
if(!TryGetOptionsType(creationOptions, configuration, driverType, out var optionsType))
33+
return null;
34+
35+
if(!TrySetOptionsCustomizer(creationOptions, configuration, optionsType))
36+
return null;
37+
38+
return creationOptions;
39+
}
40+
41+
/// <summary>
42+
/// Validates and gets the <see cref="Type"/> of the implementation of <see cref="IWebDriver"/> implementation indicated by the configuration.
43+
/// </summary>
44+
/// <remarks>
45+
/// <para>
46+
/// Note that it is valid for the driver type to be <see langword="null"/> if <see cref="WebDriverCreationOptions.DriverFactoryType"/> is specified.
47+
/// In that scenario, the driver type is unused, but it still indicates a valid configuration.
48+
/// </para>
49+
/// </remarks>
50+
/// <param name="options">The options, as they have been parsed so far</param>
51+
/// <param name="configuration">The configuration section</param>
52+
/// <param name="driverType">If this method returns <see langword="true"/> then this is a <see cref="Type"/> of the web driver, otherwise
53+
/// this value is undefined and must be ignored.</param>
54+
/// <returns><see langword="true"/> if the driver type information is valid; <see langword="false"/> if not</returns>
55+
bool TryGetDriverType(WebDriverCreationOptions options, IConfigurationSection configuration, out Type driverType)
56+
{
57+
driverType = null;
58+
if(options.DriverFactoryType != null)
59+
return true;
60+
61+
if(options.DriverType is null)
62+
{
63+
logger.LogError("{ParamName} is mandatory unless {FactoryTypeKey} is specified; the configuration '{ConfigKey}' will be omitted.",
64+
nameof(WebDriverCreationOptions.DriverType),
65+
nameof(WebDriverCreationOptions.DriverFactoryType),
66+
configuration.Key);
67+
return false;
68+
}
69+
70+
try
71+
{
72+
driverType = typeProvider.GetWebDriverType(options.DriverType);
73+
return true;
74+
}
75+
catch(Exception e)
76+
{
77+
logger.LogError(e,
78+
"No implementation of {WebDriverIface} could be found for the {DriverTypeProp} '{DriverType}'; the driver configuration '{ConfigKey}' will be omitted. " +
79+
"Reminder: If the driver type is not one which is shipped with Selenium then you must specify its assembly-qualified type name.",
80+
nameof(IWebDriver),
81+
nameof(WebDriverCreationOptions.DriverType),
82+
options.DriverType,
83+
configuration.Key);
84+
return false;
85+
}
86+
}
87+
88+
/// <summary>
89+
/// Validates and gets the <see cref="Type"/> of the implementation of <see cref="DriverOptions"/> implementation indicated by the configuration.
90+
/// </summary>
91+
/// <remarks>
92+
/// <para>
93+
/// Note that it is valid for the options type to be <see langword="null"/> if <see cref="WebDriverCreationOptions.DriverFactoryType"/> is specified.
94+
/// In that scenario, the options type is unused, but it still indicates a valid configuration.
95+
/// </para>
96+
/// </remarks>
97+
/// <param name="options">The options, as they have been parsed so far</param>
98+
/// <param name="configuration">The configuration section</param>
99+
/// <param name="driverType">The type of the Web Driver, as has already been determined by
100+
/// <see cref="TryGetDriverType(WebDriverCreationOptions, IConfigurationSection, out Type)"/>.</param>
101+
/// <param name="optionsType">If this method returns <see langword="true"/> then this is a <see cref="Type"/> of the driver options, otherwise
102+
/// this value is undefined and must be ignored.</param>
103+
/// <returns><see langword="true"/> if the driver type information is valid; <see langword="false"/> if not</returns>
104+
bool TryGetOptionsType(WebDriverCreationOptions options, IConfigurationSection configuration, Type driverType, out Type optionsType)
105+
{
106+
optionsType = null;
107+
if(options.DriverFactoryType != null)
108+
return true;
109+
110+
try
111+
{
112+
optionsType = typeProvider.GetWebDriverOptionsType(driverType, options.OptionsType);
113+
}
114+
catch(Exception e)
115+
{
116+
logger.LogError(e,
117+
"No type deriving from {OptionsBase} could be found for the combination of {WebDriverIface} {DriverType} and {OptionsTypeProp} '{OptionsType}'; the configuration '{ConfigKey}' will be omitted. " +
118+
"See the exception details for more information.",
119+
nameof(DriverOptions),
120+
nameof(IWebDriver),
121+
driverType.Name,
122+
nameof(WebDriverCreationOptions.OptionsType),
123+
options.OptionsType,
124+
configuration.Key);
125+
return false;
126+
}
127+
128+
try
129+
{
130+
options.OptionsFactory = GetOptions(optionsType, configuration);
131+
return true;
132+
}
133+
catch(Exception e)
134+
{
135+
logger.LogError(e,
136+
"An unexpected error occurred creating or binding to the {OptionsClass} type {OptionsType}; the configuration '{ConfigKey}' will be omitted.",
137+
nameof(DriverOptions),
138+
optionsType.FullName,
139+
configuration.Key);
140+
return false;
141+
}
142+
}
143+
144+
bool TrySetOptionsCustomizer(WebDriverCreationOptions options, IConfigurationSection configuration, Type optionsType)
145+
{
146+
var customizerTypeName = configuration.GetValue<string>("OptionsCustomizerType");
147+
try
148+
{
149+
options.OptionsCustomizer = GetOptionsCustomizer(optionsType, customizerTypeName);
150+
return true;
151+
}
152+
catch(Exception e)
153+
{
154+
logger.LogError(e,
155+
"An unexpected error occurred binding the {OptionsCustomizer} type {CustomizerType}; the configuration '{ConfigKey}' will be omitted.",
156+
nameof(WebDriverCreationOptions.OptionsCustomizer),
157+
customizerTypeName,
158+
configuration.Key);
159+
return false;
160+
}
161+
}
162+
163+
static Func<DriverOptions> GetOptions(Type optionsType, IConfigurationSection config)
164+
{
165+
return () =>
166+
{
167+
var options = (DriverOptions)Activator.CreateInstance(optionsType);
168+
config.Bind("Options", options);
169+
return options;
170+
};
171+
}
172+
173+
static ICustomizesOptions<DriverOptions> GetOptionsCustomizer(Type optionsType, string customizerTypeName)
174+
{
175+
if(string.IsNullOrWhiteSpace(customizerTypeName)) return null;
176+
var customizerType = Type.GetType(customizerTypeName, true);
177+
178+
if(!typeof(ICustomizesOptions<>).MakeGenericType(optionsType).IsAssignableFrom(customizerType))
179+
throw new ArgumentException($"The specified customizer type must implement {nameof(ICustomizesOptions<DriverOptions>)}<{optionsType.Name}>.", nameof(customizerTypeName));
180+
if(customizerType.GetConstructor(Type.EmptyTypes) == null)
181+
throw new ArgumentException($"The specified customizer type must have a public parameterless constructor.", nameof(customizerTypeName));
182+
183+
return (ICustomizesOptions<DriverOptions>) Activator.CreateInstance(customizerType);
184+
}
185+
186+
/// <summary>
187+
/// Initializes a new instance of the <see cref="WebDriverConfigurationItemParser"/> class.
188+
/// </summary>
189+
/// <param name="typeProvider">The provider for web driver and options types.</param>
190+
/// <param name="logger">The logger for this parser.</param>
191+
public WebDriverConfigurationItemParser(IGetsWebDriverAndOptionsTypes typeProvider,
192+
ILogger<WebDriverConfigurationItemParser> logger)
193+
{
194+
this.typeProvider = typeProvider ?? throw new ArgumentNullException(nameof(typeProvider));
195+
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
196+
}
197+
}
198+
}
199+

0 commit comments

Comments
 (0)