From 243085ecadc793102cf6fd1d3330308d876049eb Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Wed, 20 May 2026 21:28:11 +0100 Subject: [PATCH 01/19] WIP #59 - Refactor for SRP Move many responsibilities out from WebDriverConfigurationItemParser into their own types/services. This means I'm going to be able to extend them in a subsequent commit. --- .../ActivatorDriverOptionsFactory.cs | 16 +++ ...figBindingDriverOptionsFactoryDecorator.cs | 32 +++++ .../Factories/DriverTypeProvider.cs | 63 +++++++++ .../Factories/ICreatesDriverOptions.cs | 20 +++ .../Factories/IGetsDriverType.cs | 29 ++++ .../Factories/IGetsOptionsType.cs | 31 ++++ .../Factories/OptionsTypeProvider.cs | 77 ++++++++++ .../WebDriverConfigurationItemParser.cs | 132 ++---------------- .../ServiceCollectionExtensions.cs | 37 ++++- 9 files changed, 309 insertions(+), 128 deletions(-) create mode 100644 CSF.Extensions.WebDriver/Factories/ActivatorDriverOptionsFactory.cs create mode 100644 CSF.Extensions.WebDriver/Factories/ConfigBindingDriverOptionsFactoryDecorator.cs create mode 100644 CSF.Extensions.WebDriver/Factories/DriverTypeProvider.cs create mode 100644 CSF.Extensions.WebDriver/Factories/ICreatesDriverOptions.cs create mode 100644 CSF.Extensions.WebDriver/Factories/IGetsDriverType.cs create mode 100644 CSF.Extensions.WebDriver/Factories/IGetsOptionsType.cs create mode 100644 CSF.Extensions.WebDriver/Factories/OptionsTypeProvider.cs diff --git a/CSF.Extensions.WebDriver/Factories/ActivatorDriverOptionsFactory.cs b/CSF.Extensions.WebDriver/Factories/ActivatorDriverOptionsFactory.cs new file mode 100644 index 0000000..dd50de6 --- /dev/null +++ b/CSF.Extensions.WebDriver/Factories/ActivatorDriverOptionsFactory.cs @@ -0,0 +1,16 @@ +using System; +using Microsoft.Extensions.Configuration; +using OpenQA.Selenium; + +namespace CSF.Extensions.WebDriver.Factories +{ + /// + /// Implementation of which uses to create the options object. + /// + public class ActivatorDriverOptionsFactory : ICreatesDriverOptions + { + /// + public DriverOptions CreateOptions(Type optionsType, IConfigurationSection config) + => (DriverOptions) Activator.CreateInstance(optionsType); + } +} \ No newline at end of file diff --git a/CSF.Extensions.WebDriver/Factories/ConfigBindingDriverOptionsFactoryDecorator.cs b/CSF.Extensions.WebDriver/Factories/ConfigBindingDriverOptionsFactoryDecorator.cs new file mode 100644 index 0000000..d5e8d01 --- /dev/null +++ b/CSF.Extensions.WebDriver/Factories/ConfigBindingDriverOptionsFactoryDecorator.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.Extensions.Configuration; +using OpenQA.Selenium; + +namespace CSF.Extensions.WebDriver.Factories +{ + /// + /// Decorator for which binds the configuration to the created options. + /// + public class ConfigBindingDriverOptionsFactoryDecorator : ICreatesDriverOptions + { + readonly ICreatesDriverOptions wrapped; + + /// + public DriverOptions CreateOptions(Type optionsType, IConfigurationSection config) + { + var options = wrapped.CreateOptions(optionsType, config); + config.Bind("Options", options); + return options; + } + + /// + /// Initialises a new instance of . + /// + /// The wrapped service + /// If is . + public ConfigBindingDriverOptionsFactoryDecorator(ICreatesDriverOptions wrapped) + { + this.wrapped = wrapped ?? throw new ArgumentNullException(nameof(wrapped)); + } + } +} \ No newline at end of file diff --git a/CSF.Extensions.WebDriver/Factories/DriverTypeProvider.cs b/CSF.Extensions.WebDriver/Factories/DriverTypeProvider.cs new file mode 100644 index 0000000..6b72e01 --- /dev/null +++ b/CSF.Extensions.WebDriver/Factories/DriverTypeProvider.cs @@ -0,0 +1,63 @@ +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using OpenQA.Selenium; + +namespace CSF.Extensions.WebDriver.Factories +{ + /// + /// Default implementation of . + /// + public class DriverTypeProvider : IGetsDriverType + { + readonly ILogger logger; + readonly IGetsWebDriverAndOptionsTypes typeProvider; + + /// + public bool TryGetDriverType(WebDriverCreationOptions options, IConfigurationSection configuration, out Type driverType) + { + driverType = null; + + if(options.DriverType is null) + { + if(options.DriverFactoryType == null) + logger.LogError("{ParamName} is mandatory unless {FactoryTypeKey} is specified; the configuration '{ConfigKey}' will be omitted.", + nameof(WebDriverCreationOptions.DriverType), + nameof(WebDriverCreationOptions.DriverFactoryType), + configuration.Key); + return options.DriverFactoryType != null; + } + + try + { + driverType = typeProvider.GetWebDriverType(options.DriverType); + return true; + } + catch(Exception e) + { + if(options.DriverFactoryType == null) + logger.LogError(e, + "No implementation of {WebDriverIface} could be found for the {DriverTypeProp} '{DriverType}'; the driver configuration '{ConfigKey}' will be omitted. " + + "Reminder: If the driver type is not one which is shipped with Selenium then you must specify its assembly-qualified type name.", + nameof(IWebDriver), + nameof(WebDriverCreationOptions.DriverType), + options.DriverType, + configuration.Key); + return options.DriverFactoryType != null; + } + } + + /// + /// Initialises a new instance of . + /// + /// A logger + /// A provider for the concrete types of web driver and options + /// If any parameter is + public DriverTypeProvider(ILogger logger, IGetsWebDriverAndOptionsTypes typeProvider) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.typeProvider = typeProvider ?? throw new ArgumentNullException(nameof(typeProvider)); + } + } +} + diff --git a/CSF.Extensions.WebDriver/Factories/ICreatesDriverOptions.cs b/CSF.Extensions.WebDriver/Factories/ICreatesDriverOptions.cs new file mode 100644 index 0000000..341c0f8 --- /dev/null +++ b/CSF.Extensions.WebDriver/Factories/ICreatesDriverOptions.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.Extensions.Configuration; +using OpenQA.Selenium; + +namespace CSF.Extensions.WebDriver.Factories +{ + /// + /// An object which creates and returns a new object which derives from . + /// + public interface ICreatesDriverOptions + { + /// + /// Creates and returns a new driver options instance. + /// + /// The desired type of the options object + /// The CSF.Extensions.WebDriver configuration + /// A new driver options instance + DriverOptions CreateOptions(Type optionsType, IConfigurationSection config); + } +} \ No newline at end of file diff --git a/CSF.Extensions.WebDriver/Factories/IGetsDriverType.cs b/CSF.Extensions.WebDriver/Factories/IGetsDriverType.cs new file mode 100644 index 0000000..f3ec5a0 --- /dev/null +++ b/CSF.Extensions.WebDriver/Factories/IGetsDriverType.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.Extensions.Configuration; +using OpenQA.Selenium; + +namespace CSF.Extensions.WebDriver.Factories +{ + /// + /// An object which can get the concrete type of a WebDriver, indicated by the configuration. + /// + public interface IGetsDriverType + { + /// + /// Validates and gets the of the implementation of implementation indicated by the configuration. + /// + /// + /// + /// Note that it is valid for the driver type to be if is specified. + /// In that scenario, the driver type is unused, but it still indicates a valid configuration. + /// + /// + /// The options, as they have been parsed so far + /// The configuration section + /// If this method returns then this is a of the web driver, otherwise + /// this value is undefined and must be ignored. + /// if the driver type information is valid; if not + bool TryGetDriverType(WebDriverCreationOptions options, IConfigurationSection configuration, out Type driverType); + } +} + diff --git a/CSF.Extensions.WebDriver/Factories/IGetsOptionsType.cs b/CSF.Extensions.WebDriver/Factories/IGetsOptionsType.cs new file mode 100644 index 0000000..cc89fb1 --- /dev/null +++ b/CSF.Extensions.WebDriver/Factories/IGetsOptionsType.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.Extensions.Configuration; +using OpenQA.Selenium; + +namespace CSF.Extensions.WebDriver.Factories +{ + /// + /// An object which can get the concrete type of some WebDriver Options, indicated by the configuration. + /// + public interface IGetsOptionsType + { + /// + /// Validates and gets the of the implementation of implementation indicated by the configuration. + /// + /// + /// + /// Note that it is valid for the options type to be if is specified. + /// In that scenario, the options type is unused, but it still indicates a valid configuration. + /// + /// + /// The options, as they have been parsed so far + /// The configuration section + /// The type of the Web Driver, as has already been determined by + /// . + /// If this method returns then this is a of the driver options, otherwise + /// this value is undefined and must be ignored. + /// if the driver type information is valid; if not + bool TryGetOptionsType(WebDriverCreationOptions options, IConfigurationSection configuration, Type driverType, out Type optionsType); + } +} + diff --git a/CSF.Extensions.WebDriver/Factories/OptionsTypeProvider.cs b/CSF.Extensions.WebDriver/Factories/OptionsTypeProvider.cs new file mode 100644 index 0000000..a38e047 --- /dev/null +++ b/CSF.Extensions.WebDriver/Factories/OptionsTypeProvider.cs @@ -0,0 +1,77 @@ +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using OpenQA.Selenium; + +namespace CSF.Extensions.WebDriver.Factories +{ + /// + /// Default implementation of . + /// + public class OptionsTypeProvider : IGetsOptionsType + { + readonly ILogger logger; + readonly IGetsWebDriverAndOptionsTypes typeProvider; + readonly ICreatesDriverOptions optionsFactory; + + /// + public bool TryGetOptionsType(WebDriverCreationOptions options, IConfigurationSection configuration, Type driverType, out Type optionsType) + { + optionsType = null; + + try + { + optionsType = typeProvider.GetWebDriverOptionsType(driverType, options.OptionsType); + } + catch(Exception e) + { + if(options.DriverFactoryType == null) + logger.LogError(e, + "No type deriving from {OptionsBase} could be found for the combination of {WebDriverIface} {DriverType} and {OptionsTypeProp} '{OptionsType}'; the configuration '{ConfigKey}' will be omitted. " + + "See the exception details for more information.", + nameof(DriverOptions), + nameof(IWebDriver), + driverType?.Name, + nameof(WebDriverCreationOptions.OptionsType), + options?.OptionsType, + configuration.Key); + + return options.DriverFactoryType != null; + } + + try + { + options.OptionsFactory = GetOptions(optionsType, configuration); + return true; + } + catch(Exception e) + { + if(options.DriverFactoryType == null) + logger.LogError(e, + "An unexpected error occurred creating or binding to the {OptionsClass} type {OptionsType}; the configuration '{ConfigKey}' will be omitted.", + nameof(DriverOptions), + optionsType.FullName, + configuration.Key); + return options.DriverFactoryType != null; + } + } + + Func GetOptions(Type optionsType, IConfigurationSection config) + => () => optionsFactory.CreateOptions(optionsType, config); + + /// + /// Initialises a new instance of . + /// + /// A logger + /// A provider for the concrete types of web driver and options + /// A factory for instances of + /// If any parameter is + public OptionsTypeProvider(ILogger logger, IGetsWebDriverAndOptionsTypes typeProvider, ICreatesDriverOptions optionsFactory) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.typeProvider = typeProvider ?? throw new ArgumentNullException(nameof(typeProvider)); + this.optionsFactory = optionsFactory ?? throw new ArgumentNullException(nameof(optionsFactory)); + } + } +} + diff --git a/CSF.Extensions.WebDriver/Factories/WebDriverConfigurationItemParser.cs b/CSF.Extensions.WebDriver/Factories/WebDriverConfigurationItemParser.cs index 7b657ab..1bcd9d9 100644 --- a/CSF.Extensions.WebDriver/Factories/WebDriverConfigurationItemParser.cs +++ b/CSF.Extensions.WebDriver/Factories/WebDriverConfigurationItemParser.cs @@ -10,7 +10,8 @@ namespace CSF.Extensions.WebDriver.Factories /// public class WebDriverConfigurationItemParser : IParsesSingleWebDriverConfigurationSection { - readonly IGetsWebDriverAndOptionsTypes typeProvider; + readonly IGetsDriverType driverTypeProvider; + readonly IGetsOptionsType optionsTypeProvider; readonly ILogger logger; /// @@ -32,10 +33,10 @@ public WebDriverCreationOptions GetDriverConfiguration(IConfigurationSection con if(configuration.GetSection(nameof(WebDriverCreationOptions.AddBrowserQuirks)).Exists()) creationOptions.AddBrowserQuirks = configuration.GetValue(nameof(WebDriverCreationOptions.AddBrowserQuirks)); - if(!TryGetDriverType(creationOptions, configuration, out var driverType)) + if(!driverTypeProvider.TryGetDriverType(creationOptions, configuration, out var driverType)) return null; - if(!TryGetOptionsType(creationOptions, configuration, driverType, out var optionsType)) + if(!optionsTypeProvider.TryGetOptionsType(creationOptions, configuration, driverType, out var optionsType)) return null; if(!TrySetOptionsCustomizer(creationOptions, configuration, optionsType)) @@ -44,110 +45,6 @@ public WebDriverCreationOptions GetDriverConfiguration(IConfigurationSection con return creationOptions; } - /// - /// Validates and gets the of the implementation of implementation indicated by the configuration. - /// - /// - /// - /// Note that it is valid for the driver type to be if is specified. - /// In that scenario, the driver type is unused, but it still indicates a valid configuration. - /// - /// - /// The options, as they have been parsed so far - /// The configuration section - /// If this method returns then this is a of the web driver, otherwise - /// this value is undefined and must be ignored. - /// if the driver type information is valid; if not - bool TryGetDriverType(WebDriverCreationOptions options, IConfigurationSection configuration, out Type driverType) - { - driverType = null; - - if(options.DriverType is null) - { - if(options.DriverFactoryType == null) - logger.LogError("{ParamName} is mandatory unless {FactoryTypeKey} is specified; the configuration '{ConfigKey}' will be omitted.", - nameof(WebDriverCreationOptions.DriverType), - nameof(WebDriverCreationOptions.DriverFactoryType), - configuration.Key); - return options.DriverFactoryType != null ? true : false; - } - - try - { - driverType = typeProvider.GetWebDriverType(options.DriverType); - return true; - } - catch(Exception e) - { - if(options.DriverFactoryType == null) - logger.LogError(e, - "No implementation of {WebDriverIface} could be found for the {DriverTypeProp} '{DriverType}'; the driver configuration '{ConfigKey}' will be omitted. " + - "Reminder: If the driver type is not one which is shipped with Selenium then you must specify its assembly-qualified type name.", - nameof(IWebDriver), - nameof(WebDriverCreationOptions.DriverType), - options.DriverType, - configuration.Key); - return options.DriverFactoryType != null ? true : false; - } - } - - /// - /// Validates and gets the of the implementation of implementation indicated by the configuration. - /// - /// - /// - /// Note that it is valid for the options type to be if is specified. - /// In that scenario, the options type is unused, but it still indicates a valid configuration. - /// - /// - /// The options, as they have been parsed so far - /// The configuration section - /// The type of the Web Driver, as has already been determined by - /// . - /// If this method returns then this is a of the driver options, otherwise - /// this value is undefined and must be ignored. - /// if the driver type information is valid; if not - bool TryGetOptionsType(WebDriverCreationOptions options, IConfigurationSection configuration, Type driverType, out Type optionsType) - { - optionsType = null; - - try - { - optionsType = typeProvider.GetWebDriverOptionsType(driverType, options.OptionsType); - } - catch(Exception e) - { - if(options.DriverFactoryType == null) - logger.LogError(e, - "No type deriving from {OptionsBase} could be found for the combination of {WebDriverIface} {DriverType} and {OptionsTypeProp} '{OptionsType}'; the configuration '{ConfigKey}' will be omitted. " + - "See the exception details for more information.", - nameof(DriverOptions), - nameof(IWebDriver), - driverType?.Name, - nameof(WebDriverCreationOptions.OptionsType), - options?.OptionsType, - configuration.Key); - - return options.DriverFactoryType != null ? true : false; - } - - try - { - options.OptionsFactory = GetOptions(optionsType, configuration); - return true; - } - catch(Exception e) - { - if(options.DriverFactoryType == null) - logger.LogError(e, - "An unexpected error occurred creating or binding to the {OptionsClass} type {OptionsType}; the configuration '{ConfigKey}' will be omitted.", - nameof(DriverOptions), - optionsType.FullName, - configuration.Key); - return options.DriverFactoryType != null ? true : false; - } - } - bool TrySetOptionsCustomizer(WebDriverCreationOptions options, IConfigurationSection configuration, Type optionsType) { var customizerTypeName = configuration.GetValue("OptionsCustomizerType"); @@ -167,16 +64,6 @@ bool TrySetOptionsCustomizer(WebDriverCreationOptions options, IConfigurationSec } } - static Func GetOptions(Type optionsType, IConfigurationSection config) - { - return () => - { - var options = (DriverOptions)Activator.CreateInstance(optionsType); - config.Bind("Options", options); - return options; - }; - } - static object GetOptionsCustomizer(Type optionsType, string customizerTypeName) { if(string.IsNullOrWhiteSpace(customizerTypeName)) return null; @@ -189,16 +76,19 @@ static object GetOptionsCustomizer(Type optionsType, string customizerTypeName) return Activator.CreateInstance(customizerType); } - + /// /// Initializes a new instance of the class. /// - /// The provider for web driver and options types. + /// A service to get the driver type + /// A service to get the options type /// The logger for this parser. - public WebDriverConfigurationItemParser(IGetsWebDriverAndOptionsTypes typeProvider, + public WebDriverConfigurationItemParser(IGetsDriverType driverTypeProvider, + IGetsOptionsType optionsTypeProvider, ILogger logger) { - this.typeProvider = typeProvider ?? throw new ArgumentNullException(nameof(typeProvider)); + this.driverTypeProvider = driverTypeProvider ?? throw new ArgumentNullException(nameof(driverTypeProvider)); + this.optionsTypeProvider = optionsTypeProvider ?? throw new ArgumentNullException(nameof(optionsTypeProvider)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } } diff --git a/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs b/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs index d5014fc..1b9b7c8 100644 --- a/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs +++ b/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs @@ -64,12 +64,8 @@ public static IServiceCollection AddWebDriverFactory(this IServiceCollection ser string configPath = FactoryConfigPath, Action configureOptions = null) { - AddWebDriverFactoryWithoutOptionsPattern(services); - - services.AddTransient(); - services.AddTransient(); + AddWebDriverFactory(services, configureOptions); services.AddTransient(GetOptionsConfigService(configPath, configureOptions)); - services.AddOptions().Configure(configureOptions ?? (o => {})); return services; } @@ -114,12 +110,23 @@ public static IServiceCollection AddWebDriverFactory(this IServiceCollection ser public static IServiceCollection AddWebDriverFactory(this IServiceCollection services, IConfigurationSection configSection, Action configureOptions = null) + { + AddWebDriverFactory(services, configureOptions); + services.AddTransient(GetOptionsConfigService(configSection, configureOptions)); + + return services; + } + + static IServiceCollection AddWebDriverFactory(this IServiceCollection services, + Action configureOptions = null) { AddWebDriverFactoryWithoutOptionsPattern(services); services.AddTransient(); services.AddTransient(); - services.AddTransient(GetOptionsConfigService(configSection, configureOptions)); + services.AddTransient(); + services.AddTransient(); + AddDriverOptionsFactory(services); services.AddOptions().Configure(configureOptions ?? (o => {})); return services; @@ -236,7 +243,7 @@ static Func { IConfiguration configSection = services.GetRequiredService().GetSection(configPath); - return ActivatorUtilities.CreateInstance(services, configSection); + return ActivatorUtilities.CreateInstance(services, configSection, configureOptions); }; } @@ -259,6 +266,22 @@ static IServiceCollection AddLoggingIfNotAlreadyAdded(IServiceCollection service return services; } + + static IServiceCollection AddDriverOptionsFactory(IServiceCollection services) + { + services.AddTransient(); +services.AddTransient(); + + services.AddTransient(s => + { + ICreatesDriverOptions service = s.GetRequiredService(); + service = ActivatorUtilities.CreateInstance(s, service); + + return service; + }); + + return services; + } } } From 37964ae199ea152a59e9fac77dfe6fd377a6423f Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Wed, 20 May 2026 21:30:25 +0100 Subject: [PATCH 02/19] WIP #59 - Add log level support --- .../LogLevelDriverOptionsFactoryDecorator.cs | 35 +++++++++++++++++++ .../Factories/WebDriverCreationOptions.cs | 21 +++++++++++ .../ServiceCollectionExtensions.cs | 4 ++- 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 CSF.Extensions.WebDriver/Factories/LogLevelDriverOptionsFactoryDecorator.cs diff --git a/CSF.Extensions.WebDriver/Factories/LogLevelDriverOptionsFactoryDecorator.cs b/CSF.Extensions.WebDriver/Factories/LogLevelDriverOptionsFactoryDecorator.cs new file mode 100644 index 0000000..55bb5ff --- /dev/null +++ b/CSF.Extensions.WebDriver/Factories/LogLevelDriverOptionsFactoryDecorator.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.Extensions.Configuration; +using OpenQA.Selenium; + +namespace CSF.Extensions.WebDriver.Factories +{ + /// + /// Decorator for which conditionally sets the logging preference for the + /// options, based upon . + /// + public class LogLevelDriverOptionsFactoryDecorator : ICreatesDriverOptions + { + readonly ICreatesDriverOptions wrapped; + + /// + public DriverOptions CreateOptions(Type optionsType, IConfigurationSection config) + { + var options = wrapped.CreateOptions(optionsType, config); + var logLevel = config.GetValue(nameof(WebDriverCreationOptions.BrowserLogLevel)); + if(logLevel != null && Enum.TryParse(logLevel, out var parsedLevel)) + options.SetLoggingPreference(LogType.Browser, parsedLevel); + return options; + } + + /// + /// Initialises a new instance of . + /// + /// The wrapped service + /// If is . + public LogLevelDriverOptionsFactoryDecorator(ICreatesDriverOptions wrapped) + { + this.wrapped = wrapped ?? throw new ArgumentNullException(nameof(wrapped)); + } + } +} \ No newline at end of file diff --git a/CSF.Extensions.WebDriver/Factories/WebDriverCreationOptions.cs b/CSF.Extensions.WebDriver/Factories/WebDriverCreationOptions.cs index f07cff9..9c98414 100644 --- a/CSF.Extensions.WebDriver/Factories/WebDriverCreationOptions.cs +++ b/CSF.Extensions.WebDriver/Factories/WebDriverCreationOptions.cs @@ -276,6 +276,27 @@ public Func OptionsFactory /// public bool AddBrowserQuirks { get; set; } = true; + /// + /// Gets or sets a value which indicates the logging level that the web browser should retain within its console. + /// + /// + /// + /// This setting is effective only for browsers which provide direct access to logs. At the time of writing this is only + /// Chromium-based browsers such as Chrome or Edge. + /// + /// + /// The value of this property must correspond to the string representation of a Selenium . + /// If this value is then the log level will be left at the browser's default. + /// + /// + /// The setting from this option (if set) will be used via a call to options.SetLoggingPreference(LogType.Browser, LOG_LEVEL); + /// where: options is the object used to create the web driver, and LOG_LEVEL is an enum value + /// derived from the value of this property. The actual supported values for LOG_LEVEL are browser-specific, for the target web + /// browser. + /// + /// + public string BrowserLogLevel { get; set; } + static Func GetUnsetOptionsFactory() { return () => throw new InvalidOperationException($"Driver options cannot be created via {nameof(OptionsFactory)}; either {nameof(DriverType)} must be set to a type which indicates a deterministic options type or {nameof(OptionsType)} must set set. If you are using a custom {nameof(DriverFactoryType)} then it may not be appropriate to create options in this way."); diff --git a/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs b/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs index 1b9b7c8..10320b7 100644 --- a/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs +++ b/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs @@ -270,12 +270,14 @@ static IServiceCollection AddLoggingIfNotAlreadyAdded(IServiceCollection service static IServiceCollection AddDriverOptionsFactory(IServiceCollection services) { services.AddTransient(); -services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddTransient(s => { ICreatesDriverOptions service = s.GetRequiredService(); service = ActivatorUtilities.CreateInstance(s, service); + service = ActivatorUtilities.CreateInstance(s, service); return service; }); From e35d41c4810c880ed8ee161ed6f3204a3dfe8b54 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Wed, 20 May 2026 22:49:05 +0100 Subject: [PATCH 03/19] WIP #59 - Fix tests Also add new tests for new types --- .../ConfigurationFactory.cs | 25 +++++ .../Factories/DriverTypeProviderTests.cs | 59 ++++++++++++ .../Factories/OptionsTypeProviderTests.cs | 70 ++++++++++++++ .../Factories/StandardTypesAttribute.cs | 91 +++++++++++++++++-- .../WebDriverCreationConfigureOptionsTests.cs | 90 +++++++++--------- .../WebDriverConfigurationItemParser.cs | 6 ++ .../WebDriverCreationConfigureOptions.cs | 16 +++- .../Factories/WebDriverCreationOptions.cs | 2 +- .../ServiceCollectionExtensions.cs | 11 ++- 9 files changed, 311 insertions(+), 59 deletions(-) create mode 100644 CSF.Extensions.WebDriver.Tests/ConfigurationFactory.cs create mode 100644 CSF.Extensions.WebDriver.Tests/Factories/DriverTypeProviderTests.cs create mode 100644 CSF.Extensions.WebDriver.Tests/Factories/OptionsTypeProviderTests.cs diff --git a/CSF.Extensions.WebDriver.Tests/ConfigurationFactory.cs b/CSF.Extensions.WebDriver.Tests/ConfigurationFactory.cs new file mode 100644 index 0000000..ad49df0 --- /dev/null +++ b/CSF.Extensions.WebDriver.Tests/ConfigurationFactory.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Configuration; + +namespace CSF.Extensions.WebDriver; + +public static class ConfigurationFactory +{ + /// + /// Helper method to create an from a specified JSON string. + /// + /// A JSON string which will be used as the basis for the returned config. + /// A task exposing a configuration object, created from the JSON string. + public static async Task GetConfigurationAsync(string jsonConfig) + { + var builder = new ConfigurationBuilder(); + + var stream = new MemoryStream (); + using var writer = new StreamWriter(stream, leaveOpen: true); + await writer.WriteAsync(jsonConfig); + await writer.FlushAsync(); + stream.Position = 0; + + builder.AddJsonStream(stream); + return builder.Build(); + } +} \ No newline at end of file diff --git a/CSF.Extensions.WebDriver.Tests/Factories/DriverTypeProviderTests.cs b/CSF.Extensions.WebDriver.Tests/Factories/DriverTypeProviderTests.cs new file mode 100644 index 0000000..d0efa61 --- /dev/null +++ b/CSF.Extensions.WebDriver.Tests/Factories/DriverTypeProviderTests.cs @@ -0,0 +1,59 @@ +using Microsoft.Extensions.Configuration; +using OpenQA.Selenium.Chrome; + +namespace CSF.Extensions.WebDriver.Factories; + +[TestFixture, Parallelizable] +public class DriverTypeProviderTests +{ + [Test, AutoMoqData] + public void TryGetDriverTypeShouldReturnTrueForAKnownType([StandardTypes] DriverTypeProvider sut, + WebDriverCreationOptions options, + IConfigurationSection config) + { + options.DriverType = nameof(ChromeDriver); + Assert.That(sut.TryGetDriverType(options, config, out _), Is.True); + } + + [Test, AutoMoqData] + public void TryGetDriverTypeShouldReturnFalseForAnUnknownType([StandardTypes] DriverTypeProvider sut, + WebDriverCreationOptions options, + IConfigurationSection config) + { + options.DriverType = "Elephant"; + options.DriverFactoryType = null; + Assert.That(sut.TryGetDriverType(options, config, out _), Is.False); + } + + [Test, AutoMoqData] + public void TryGetDriverTypeShouldReturnFalseIfDriverTypeIsNullAndDriverFactoryIsToo([StandardTypes] DriverTypeProvider sut, + WebDriverCreationOptions options, + IConfigurationSection config) + { + options.DriverType = null; + options.DriverFactoryType = null; + Assert.That(sut.TryGetDriverType(options, config, out _), Is.False); + } + + [Test, AutoMoqData] + public void TryGetDriverTypeShouldReturnTrueForAnUnknownTypeIfDriverFactoryIsNotNull([StandardTypes] DriverTypeProvider sut, + WebDriverCreationOptions options, + IConfigurationSection config, + string factoryType) + { + options.DriverType = "Elephant"; + options.DriverFactoryType = factoryType; + Assert.That(sut.TryGetDriverType(options, config, out _), Is.True); + } + + [Test, AutoMoqData] + public void TryGetDriverTypeShouldReturnTrueIfDriverTypeIsNullIfDriverFactoryIsNotNull([StandardTypes] DriverTypeProvider sut, + WebDriverCreationOptions options, + IConfigurationSection config, + string factoryType) + { + options.DriverType = null; + options.DriverFactoryType = factoryType; + Assert.That(sut.TryGetDriverType(options, config, out _), Is.True); + } +} \ No newline at end of file diff --git a/CSF.Extensions.WebDriver.Tests/Factories/OptionsTypeProviderTests.cs b/CSF.Extensions.WebDriver.Tests/Factories/OptionsTypeProviderTests.cs new file mode 100644 index 0000000..ab6ec85 --- /dev/null +++ b/CSF.Extensions.WebDriver.Tests/Factories/OptionsTypeProviderTests.cs @@ -0,0 +1,70 @@ +using Microsoft.Extensions.Configuration; +using OpenQA.Selenium.Chrome; +using OpenQA.Selenium.Remote; + +namespace CSF.Extensions.WebDriver.Factories; + +[TestFixture, Parallelizable] +public class OptionsTypeProviderTests +{ + [Test, AutoMoqData] + public void TryGetOptionsTypeShouldReturnTrueForAKnownType([StandardTypes] OptionsTypeProvider sut, + WebDriverCreationOptions options, + IConfigurationSection config) + { + options.OptionsType = nameof(ChromeOptions); + Assert.That(sut.TryGetOptionsType(options, config, typeof(ChromeDriver), out _), Is.True); + } + + [Test, AutoMoqData] + public void TryGetOptionsTypeShouldReturnFalseForAnUnknownType([StandardTypes] OptionsTypeProvider sut, + WebDriverCreationOptions options, + IConfigurationSection config) + { + options.OptionsType = "Elephant"; + options.DriverFactoryType = null; + Assert.That(sut.TryGetOptionsType(options, config, typeof(ChromeDriver), out _), Is.False); + } + + [Test, AutoMoqData] + public void TryGetOptionsTypeShouldReturnTrueIfOptionsTypeIsNullButDriverIsAKnownType([StandardTypes] OptionsTypeProvider sut, + WebDriverCreationOptions options, + IConfigurationSection config) + { + options.OptionsType = null; + options.DriverFactoryType = null; + Assert.That(sut.TryGetOptionsType(options, config, typeof(ChromeDriver), out _), Is.True); + } + + [Test, AutoMoqData] + public void TryGetOptionsTypeShouldReturnFalseIfOptionsTypeIsNullButDriverIsAnUnknownType([StandardTypes] OptionsTypeProvider sut, + WebDriverCreationOptions options, + IConfigurationSection config) + { + options.OptionsType = null; + options.DriverFactoryType = null; + Assert.That(sut.TryGetOptionsType(options, config, typeof(RemoteWebDriver), out _), Is.False); + } + + [Test, AutoMoqData] + public void TryGetOptionsTypeShouldReturnTrueForAnUnknownTypeIfDriverFactoryTypeIsNotNull([StandardTypes] OptionsTypeProvider sut, + WebDriverCreationOptions options, + IConfigurationSection config, + string factoryType) + { + options.OptionsType = "Elephant"; + options.DriverFactoryType = factoryType; + Assert.That(sut.TryGetOptionsType(options, config, typeof(ChromeDriver), out _), Is.True); + } + + [Test, AutoMoqData] + public void TryGetOptionsTypeShouldReturnTrueIfOptionsTypeIsNullIfDriverFactoryIsNotNull([StandardTypes] OptionsTypeProvider sut, + WebDriverCreationOptions options, + IConfigurationSection config, + string factoryType) + { + options.OptionsType = null; + options.DriverFactoryType = factoryType; + Assert.That(sut.TryGetOptionsType(options, config, typeof(RemoteWebDriver), out _), Is.True); + } +} \ No newline at end of file diff --git a/CSF.Extensions.WebDriver.Tests/Factories/StandardTypesAttribute.cs b/CSF.Extensions.WebDriver.Tests/Factories/StandardTypesAttribute.cs index 0da317f..a0d57c0 100644 --- a/CSF.Extensions.WebDriver.Tests/Factories/StandardTypesAttribute.cs +++ b/CSF.Extensions.WebDriver.Tests/Factories/StandardTypesAttribute.cs @@ -1,5 +1,6 @@ using System.Reflection; using AutoFixture; +using Microsoft.Extensions.Configuration; using OpenQA.Selenium.Chrome; using OpenQA.Selenium.Firefox; using OpenQA.Selenium.Remote; @@ -25,19 +26,95 @@ public override ICustomization GetCustomization(ParameterInfo parameter) public class StandardTypesCustomization : ICustomization { + static readonly Type + chromeDriverType = typeof(ChromeDriver), + firefoxDriverType = typeof(FirefoxDriver), + remoteDriverType = typeof(RemoteWebDriver), + safariDriverType = typeof(SafariDriver), + chromeOptionsType = typeof(ChromeOptions), + firefoxOptionsType = typeof(FirefoxOptions), + safariOptionsType = typeof(SafariOptions); + public void Customize(IFixture fixture) + { + CustomizeDriverAndOptionsTypeProvider(fixture); + CustomizeOptionsTypeProvider(fixture); + CustomizeDriverType(fixture); + } + + static void CustomizeDriverAndOptionsTypeProvider(IFixture fixture) { fixture.Customize(c => c.FromFactory(() => { var mock = new Mock(MockBehavior.Strict); - mock.Setup(x => x.GetWebDriverType(nameof(ChromeDriver))).Returns(typeof(ChromeDriver)); - mock.Setup(x => x.GetWebDriverType(nameof(FirefoxDriver))).Returns(typeof(FirefoxDriver)); - mock.Setup(x => x.GetWebDriverType(nameof(RemoteWebDriver))).Returns(typeof(RemoteWebDriver)); - mock.Setup(x => x.GetWebDriverType(nameof(SafariDriver))).Returns(typeof(SafariDriver)); - mock.Setup(x => x.GetWebDriverOptionsType(typeof(ChromeDriver), null)).Returns(typeof(ChromeOptions)); - mock.Setup(x => x.GetWebDriverOptionsType(typeof(FirefoxDriver), null)).Returns(typeof(FirefoxOptions)); - mock.Setup(x => x.GetWebDriverOptionsType(typeof(SafariDriver), null)).Returns(typeof(SafariOptions)); + mock.Setup(x => x.GetWebDriverType(nameof(ChromeDriver))).Returns(chromeDriverType); + mock.Setup(x => x.GetWebDriverType(nameof(FirefoxDriver))).Returns(firefoxDriverType); + mock.Setup(x => x.GetWebDriverType(nameof(RemoteWebDriver))).Returns(remoteDriverType); + mock.Setup(x => x.GetWebDriverType(nameof(SafariDriver))).Returns(safariDriverType); + mock.Setup(x => x.GetWebDriverOptionsType(chromeDriverType, null)).Returns(chromeOptionsType); + mock.Setup(x => x.GetWebDriverOptionsType(firefoxDriverType, null)).Returns(firefoxOptionsType); + mock.Setup(x => x.GetWebDriverOptionsType(safariDriverType, null)).Returns(safariOptionsType); return mock.Object; })); + fixture.Freeze(); } + + static void CustomizeOptionsTypeProvider(IFixture fixture) + { + fixture.Customize(c => c.FromFactory(() => + { + var mock = new Mock(MockBehavior.Strict); + Type nullType = null!; + mock + .Setup(x => x.TryGetOptionsType(It.IsAny(), It.IsAny(), It.IsAny(), out nullType)) + .Returns(false); + var chromeType = chromeOptionsType; + mock + .Setup(x => x.TryGetOptionsType(It.IsAny(), It.IsAny(), chromeDriverType, out chromeType)) + .Returns(true); + var firefoxType = firefoxOptionsType; + mock + .Setup(x => x.TryGetOptionsType(It.IsAny(), It.IsAny(), firefoxDriverType, out firefoxType)) + .Returns(true); + var safariType = safariOptionsType; + mock + .Setup(x => x.TryGetOptionsType(It.IsAny(), It.IsAny(), safariDriverType, out safariType)) + .Returns(true); + return mock.Object; + })); + + fixture.Freeze(); + } + + static void CustomizeDriverType(IFixture fixture) + { + fixture.Customize(c => c.FromFactory(() => + { + var mock = new Mock(MockBehavior.Strict); + Type nullType = null!; + mock + .Setup(x => x.TryGetDriverType(It.IsAny(), It.IsAny(), out nullType)) + .Returns(false); + var chromeType = chromeDriverType; + mock + .Setup(x => x.TryGetDriverType(It.Is(o => o.DriverType == nameof(ChromeDriver)), It.IsAny(), out chromeType)) + .Returns(true); + var firefoxType = firefoxDriverType; + mock + .Setup(x => x.TryGetDriverType(It.Is(o => o.DriverType == nameof(FirefoxDriver)), It.IsAny(), out firefoxType)) + .Returns(true); + var remoteType = remoteDriverType; + mock + .Setup(x => x.TryGetDriverType(It.Is(o => o.DriverType == nameof(RemoteWebDriver)), It.IsAny(), out remoteType)) + .Returns(true); + var safariType = safariDriverType; + mock + .Setup(x => x.TryGetDriverType(It.Is(o => o.DriverType == nameof(SafariDriver)), It.IsAny(), out safariType)) + .Returns(true); + + return mock.Object; + })); + + fixture.Freeze(); + } } \ No newline at end of file diff --git a/CSF.Extensions.WebDriver.Tests/Factories/WebDriverCreationConfigureOptionsTests.cs b/CSF.Extensions.WebDriver.Tests/Factories/WebDriverCreationConfigureOptionsTests.cs index 59a190b..a8a1115 100644 --- a/CSF.Extensions.WebDriver.Tests/Factories/WebDriverCreationConfigureOptionsTests.cs +++ b/CSF.Extensions.WebDriver.Tests/Factories/WebDriverCreationConfigureOptionsTests.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using OpenQA.Selenium; using OpenQA.Selenium.Chrome; using OpenQA.Selenium.Firefox; using OpenQA.Selenium.Remote; @@ -11,9 +12,10 @@ namespace CSF.Extensions.WebDriver.Factories; public class WebDriverCreationConfigureOptionsTests { [Test,AutoMoqData] - public async Task ConfigureShouldBeAbleToSetupLocalChromeDriverWithSimpleOptionsFromJsonConfiguration([StandardTypes] IGetsWebDriverAndOptionsTypes typeProvider) + public async Task ConfigureShouldBeAbleToSetupLocalChromeDriverWithSimpleOptionsFromJsonConfiguration([StandardTypes] IGetsDriverType driverTypeProvider, + [StandardTypes] IGetsOptionsType optionsTypeProvider) { - var options = await GetOptionsAsync(typeProvider, + var options = await GetOptionsAsync(driverTypeProvider, optionsTypeProvider, @"{ ""DriverConfigurations"": { ""Test"": { @@ -47,9 +49,10 @@ public async Task ConfigureShouldBeAbleToSetupLocalChromeDriverWithSimpleOptions [Test,AutoMoqData] - public async Task ConfigureShouldBeAbleToSetupTwoLocalDriversWithSimpleOptionsFromJsonConfiguration([StandardTypes] IGetsWebDriverAndOptionsTypes typeProvider) + public async Task ConfigureShouldBeAbleToSetupTwoLocalDriversWithSimpleOptionsFromJsonConfiguration([StandardTypes] IGetsDriverType driverTypeProvider, + [StandardTypes] IGetsOptionsType optionsTypeProvider) { - var options = await GetOptionsAsync(typeProvider, + var options = await GetOptionsAsync(driverTypeProvider, optionsTypeProvider, @"{ ""DriverConfigurations"": { ""SampleChrome"": { @@ -87,9 +90,10 @@ public async Task ConfigureShouldBeAbleToSetupTwoLocalDriversWithSimpleOptionsFr } [Test,AutoMoqData] - public async Task ConfigureShouldBeAbleToSetupLocalChromeDriverWithNoOptionsFromJsonConfiguration([StandardTypes] IGetsWebDriverAndOptionsTypes typeProvider) + public async Task ConfigureShouldBeAbleToSetupLocalChromeDriverWithNoOptionsFromJsonConfiguration([StandardTypes] IGetsDriverType driverTypeProvider, + [StandardTypes] IGetsOptionsType optionsTypeProvider) { - var options = await GetOptionsAsync(typeProvider, + var options = await GetOptionsAsync(driverTypeProvider, optionsTypeProvider, @"{ ""DriverConfigurations"": { ""Test"": { ""DriverType"": ""ChromeDriver"" } @@ -110,9 +114,10 @@ public async Task ConfigureShouldBeAbleToSetupLocalChromeDriverWithNoOptionsFrom } [Test,AutoMoqData] - public async Task ConfigureShouldBeAbleToGetSelectedConfigWhenThereIsOnlyOnePresent([StandardTypes] IGetsWebDriverAndOptionsTypes typeProvider) + public async Task ConfigureShouldBeAbleToGetSelectedConfigWhenThereIsOnlyOnePresent([StandardTypes] IGetsDriverType driverTypeProvider, + [StandardTypes] IGetsOptionsType optionsTypeProvider) { - var options = await GetOptionsAsync(typeProvider, + var options = await GetOptionsAsync(driverTypeProvider, optionsTypeProvider, @"{ ""DriverConfigurations"": { ""Test"": { ""DriverType"": ""ChromeDriver"" } @@ -123,9 +128,10 @@ public async Task ConfigureShouldBeAbleToGetSelectedConfigWhenThereIsOnlyOnePres } [Test,AutoMoqData] - public async Task ConfigureShouldBeAbleToAddACustomizerToSomeOptions([StandardTypes] IGetsWebDriverAndOptionsTypes typeProvider) + public async Task ConfigureShouldBeAbleToAddACustomizerToSomeOptions([StandardTypes] IGetsDriverType driverTypeProvider, + [StandardTypes] IGetsOptionsType optionsTypeProvider) { - var options = await GetOptionsAsync(typeProvider, + var options = await GetOptionsAsync(driverTypeProvider, optionsTypeProvider, @"{ ""DriverConfigurations"": { ""Test"": { ""DriverType"": ""ChromeDriver"", ""OptionsCustomizerType"": ""CSF.Extensions.WebDriver.Factories.SampleCustomizer, CSF.Extensions.WebDriver.Tests"" } @@ -136,9 +142,10 @@ public async Task ConfigureShouldBeAbleToAddACustomizerToSomeOptions([StandardTy } [Test,AutoMoqData] - public async Task ConfigureShouldBeAbleToGetSelectedConfigWhenASelectedConfigIsNamed([StandardTypes] IGetsWebDriverAndOptionsTypes typeProvider) + public async Task ConfigureShouldBeAbleToGetSelectedConfigWhenASelectedConfigIsNamed([StandardTypes] IGetsDriverType driverTypeProvider, + [StandardTypes] IGetsOptionsType optionsTypeProvider) { - var options = await GetOptionsAsync(typeProvider, + var options = await GetOptionsAsync(driverTypeProvider, optionsTypeProvider, @"{ ""DriverConfigurations"": { ""Test"": { ""DriverType"": ""ChromeDriver"" }, @@ -151,9 +158,10 @@ public async Task ConfigureShouldBeAbleToGetSelectedConfigWhenASelectedConfigIsN } [Test,AutoMoqData] - public async Task ConfigureShouldProvideThrowWhenThereAreTwoConfigsAndNoExplicitSelection([StandardTypes] IGetsWebDriverAndOptionsTypes typeProvider) + public async Task ConfigureShouldProvideThrowWhenThereAreTwoConfigsAndNoExplicitSelection([StandardTypes] IGetsDriverType driverTypeProvider, + [StandardTypes] IGetsOptionsType optionsTypeProvider) { - var options = await GetOptionsAsync(typeProvider, + var options = await GetOptionsAsync(driverTypeProvider, optionsTypeProvider, @"{ ""DriverConfigurations"": { ""Test"": { ""DriverType"": ""ChromeDriver"" }, @@ -165,9 +173,10 @@ public async Task ConfigureShouldProvideThrowWhenThereAreTwoConfigsAndNoExplicit } [Test,AutoMoqData] - public async Task ConfigureShouldNotThrowForANonsenseDriverType([StandardTypes] IGetsWebDriverAndOptionsTypes typeProvider) + public async Task ConfigureShouldNotThrowForANonsenseDriverType([StandardTypes] IGetsDriverType driverTypeProvider, + [StandardTypes] IGetsOptionsType optionsTypeProvider) { - var options = await GetOptionsAsync(typeProvider, + var options = await GetOptionsAsync(driverTypeProvider, optionsTypeProvider, @"{ ""DriverConfigurations"": { ""Test"": { ""DriverType"": ""NonexistentDriver"" } @@ -178,9 +187,10 @@ public async Task ConfigureShouldNotThrowForANonsenseDriverType([StandardTypes] } [Test,AutoMoqData] - public async Task ConfigureShouldNotThrowForARemoteDriverWithoutOptionsType([StandardTypes] IGetsWebDriverAndOptionsTypes typeProvider) + public async Task ConfigureShouldNotThrowForARemoteDriverWithoutOptionsType([StandardTypes] IGetsDriverType driverTypeProvider, + [StandardTypes] IGetsOptionsType optionsTypeProvider) { - var options = await GetOptionsAsync(typeProvider, + var options = await GetOptionsAsync(driverTypeProvider, optionsTypeProvider, @"{ ""DriverConfigurations"": { ""Test"": { ""DriverType"": ""RemoteWebDriver"" } @@ -191,14 +201,15 @@ public async Task ConfigureShouldNotThrowForARemoteDriverWithoutOptionsType([Sta } [Test,AutoMoqData] - public async Task ConfigureShouldBeAbleToSetupRemoteDriverWithSimpleOptionsFromJsonConfiguration([StandardTypes] IGetsWebDriverAndOptionsTypes typeProvider, + public async Task ConfigureShouldBeAbleToSetupRemoteDriverWithSimpleOptionsFromJsonConfiguration([StandardTypes] IGetsDriverType driverTypeProvider, + [StandardTypes] IGetsOptionsType optionsTypeProvider, [TestLogger] ILogger logger) { - Mock.Get(typeProvider) - .Setup(x => x.GetWebDriverOptionsType(typeof(RemoteWebDriver), "SafariOptions")) - .Returns(typeof(SafariOptions)); - - var options = await GetOptionsAsync(typeProvider, + var type = typeof(SafariOptions); + Mock.Get(optionsTypeProvider) + .Setup(x => x.TryGetOptionsType(It.Is(o => o.OptionsType == nameof(SafariOptions)), It.IsAny(), typeof(RemoteWebDriver), out type)) + .Returns(true); + var options = await GetOptionsAsync(driverTypeProvider, optionsTypeProvider, @"{ ""DriverConfigurations"": { ""Test"": { ""DriverType"": ""RemoteWebDriver"", ""OptionsType"": ""SafariOptions"" } @@ -208,24 +219,6 @@ public async Task ConfigureShouldBeAbleToSetupRemoteDriverWithSimpleOptionsFromJ Assert.That(options.DriverConfigurations, Is.Not.Empty); } - /// - /// Helper method to create an from a specified JSON string. - /// - /// A JSON string which will be used as the basis for the returned config. - /// A task exposing a configuration object, created from the JSON string. - static async Task GetConfigurationAsync(string jsonConfig) - { - var builder = new ConfigurationBuilder(); - - var stream = new MemoryStream (); - using var writer = new StreamWriter(stream, leaveOpen: true); - await writer.WriteAsync(jsonConfig); - await writer.FlushAsync(); - stream.Position = 0; - - builder.AddJsonStream(stream); - return builder.Build(); - } /// /// Creates and exercises in order to create a new @@ -234,15 +227,20 @@ static async Task GetConfigurationAsync(string jsonConfig) /// The type provider for web driver and options types /// The JSON config from which to create the options /// A task exposing the webdriver creation options collection, configured by the SUT - static async Task GetOptionsAsync(IGetsWebDriverAndOptionsTypes typeProvider, + static async Task GetOptionsAsync(IGetsDriverType driverTypeProvider, + IGetsOptionsType optionsTypeProvider, string json, ILogger? logger = null) { var options = new WebDriverCreationOptionsCollection(); - var config = await GetConfigurationAsync(json); - var sut = new WebDriverCreationConfigureOptions(new WebDriverConfigurationItemParser(typeProvider, Mock.Of>()), + var config = await ConfigurationFactory.GetConfigurationAsync(json); + ICreatesDriverOptions optionsFactory = new ActivatorDriverOptionsFactory(); + optionsFactory = new ConfigBindingDriverOptionsFactoryDecorator(optionsFactory); + var parser = new WebDriverConfigurationItemParser(driverTypeProvider, optionsTypeProvider, optionsFactory, Mock.Of>()); + var sut = new WebDriverCreationConfigureOptions(parser, config, - logger ?? Mock.Of>()); + logger ?? Mock.Of>(), + c => {}); sut.Configure(options); return options; } diff --git a/CSF.Extensions.WebDriver/Factories/WebDriverConfigurationItemParser.cs b/CSF.Extensions.WebDriver/Factories/WebDriverConfigurationItemParser.cs index 1bcd9d9..e07bedf 100644 --- a/CSF.Extensions.WebDriver/Factories/WebDriverConfigurationItemParser.cs +++ b/CSF.Extensions.WebDriver/Factories/WebDriverConfigurationItemParser.cs @@ -12,6 +12,7 @@ public class WebDriverConfigurationItemParser : IParsesSingleWebDriverConfigurat { readonly IGetsDriverType driverTypeProvider; readonly IGetsOptionsType optionsTypeProvider; + readonly ICreatesDriverOptions optionsFactory; readonly ILogger logger; /// @@ -39,6 +40,8 @@ public WebDriverCreationOptions GetDriverConfiguration(IConfigurationSection con if(!optionsTypeProvider.TryGetOptionsType(creationOptions, configuration, driverType, out var optionsType)) return null; + creationOptions.OptionsFactory = () => optionsFactory.CreateOptions(optionsType, configuration); + if(!TrySetOptionsCustomizer(creationOptions, configuration, optionsType)) return null; @@ -82,13 +85,16 @@ static object GetOptionsCustomizer(Type optionsType, string customizerTypeName) /// /// A service to get the driver type /// A service to get the options type + /// A service to get the driver options /// The logger for this parser. public WebDriverConfigurationItemParser(IGetsDriverType driverTypeProvider, IGetsOptionsType optionsTypeProvider, + ICreatesDriverOptions optionsFactory, ILogger logger) { this.driverTypeProvider = driverTypeProvider ?? throw new ArgumentNullException(nameof(driverTypeProvider)); this.optionsTypeProvider = optionsTypeProvider ?? throw new ArgumentNullException(nameof(optionsTypeProvider)); + this.optionsFactory = optionsFactory ?? throw new ArgumentNullException(nameof(optionsFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } } diff --git a/CSF.Extensions.WebDriver/Factories/WebDriverCreationConfigureOptions.cs b/CSF.Extensions.WebDriver/Factories/WebDriverCreationConfigureOptions.cs index 8266383..6c63d95 100644 --- a/CSF.Extensions.WebDriver/Factories/WebDriverCreationConfigureOptions.cs +++ b/CSF.Extensions.WebDriver/Factories/WebDriverCreationConfigureOptions.cs @@ -25,13 +25,20 @@ public sealed class WebDriverCreationConfigureOptions : IConfigureOptions logger; + readonly Action configureOptions; /// public void Configure(WebDriverCreationOptionsCollection options) + { + ConfigureUsingConfig(options); + configureOptions?.Invoke(options); + } + + void ConfigureUsingConfig(WebDriverCreationOptionsCollection options) { if(configuration is null) { - logger.LogWarning("Configuration for {TypeName} is null; the WebDriver creation options will be left unconfigured. " + + logger.LogWarning("Configuration for {TypeName} is null; the WebDriver creation options will not be configured from any configuration source. " + "Reminder: By default the configuration path is '{Path}'.", nameof(WebDriverCreationOptionsCollection), ServiceCollectionExtensions.FactoryConfigPath); @@ -50,7 +57,7 @@ IDictionary GetDriverConfigurations(IConfigura .Where(x => x.Value != null) .ToDictionary(k => k.Key, v => v.Value); - + /// /// Initialises a new instance of . @@ -58,14 +65,17 @@ IDictionary GetDriverConfigurations(IConfigura /// A parser for a single configuration item. /// The app configuration. /// A logging implementation. + /// An optional configuration action/callback to further configure the options /// If either parameter is . public WebDriverCreationConfigureOptions(IParsesSingleWebDriverConfigurationSection configParser, IConfiguration configuration, - ILogger logger) + ILogger logger, + Action configureOptions) { this.configParser = configParser ?? throw new ArgumentNullException(nameof(configParser)); this.configuration = configuration; this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.configureOptions = configureOptions; } } } \ No newline at end of file diff --git a/CSF.Extensions.WebDriver/Factories/WebDriverCreationOptions.cs b/CSF.Extensions.WebDriver/Factories/WebDriverCreationOptions.cs index 9c98414..fac6b9b 100644 --- a/CSF.Extensions.WebDriver/Factories/WebDriverCreationOptions.cs +++ b/CSF.Extensions.WebDriver/Factories/WebDriverCreationOptions.cs @@ -299,7 +299,7 @@ public Func OptionsFactory static Func GetUnsetOptionsFactory() { - return () => throw new InvalidOperationException($"Driver options cannot be created via {nameof(OptionsFactory)}; either {nameof(DriverType)} must be set to a type which indicates a deterministic options type or {nameof(OptionsType)} must set set. If you are using a custom {nameof(DriverFactoryType)} then it may not be appropriate to create options in this way."); + return () => throw new InvalidOperationException($"Driver options cannot be created via {nameof(OptionsFactory)}; either {nameof(DriverType)} must be set to a type which indicates a deterministic options type or {nameof(OptionsType)} must be set. If you are using a custom {nameof(DriverFactoryType)} then it may not be appropriate to create options in this way."); } } } \ No newline at end of file diff --git a/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs b/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs index 10320b7..c3a8c1e 100644 --- a/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs +++ b/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs @@ -128,6 +128,7 @@ static IServiceCollection AddWebDriverFactory(this IServiceCollection services, services.AddTransient(); AddDriverOptionsFactory(services); services.AddOptions().Configure(configureOptions ?? (o => {})); + services.AddTransient(); return services; } @@ -243,7 +244,10 @@ static Func { IConfiguration configSection = services.GetRequiredService().GetSection(configPath); - return ActivatorUtilities.CreateInstance(services, configSection, configureOptions); + return new WebDriverCreationConfigureOptions(services.GetRequiredService(), + configSection, + services.GetRequiredService>(), + configureOptions); }; } @@ -252,7 +256,10 @@ static Func { - return ActivatorUtilities.CreateInstance(services, configSection, configureOptions); + return new WebDriverCreationConfigureOptions(services.GetRequiredService(), + configSection, + services.GetRequiredService>(), + configureOptions); }; } From 8095045a8171497c9e394c16d3173ef441646352 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 21 May 2026 13:35:46 +0100 Subject: [PATCH 04/19] Add SonarQube analysis --- .github/workflows/dotnetCi.yml | 42 ++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnetCi.yml b/.github/workflows/dotnetCi.yml index 4d9a126..4f8811b 100644 --- a/.github/workflows/dotnetCi.yml +++ b/.github/workflows/dotnetCi.yml @@ -24,10 +24,18 @@ jobs: timeout-minutes: 30 env: + RunNumber: ${{ github.run_number }}.${{ github.run_attempt }} VersionSuffix: ci.${{ github.run_number }} + SonarCloudProject: csf-dev_CSF.Extensions.WebDriver + SonarCloudUsername: craigfowler-github + SonarCloudUrl: https://sonarcloud.io + SonarCloudSecretKey: ${{ secrets.SONARCLOUDKEY }} Configuration: Debug DotnetVersion: 8.0.x DISPLAY: :99 + BranchName: ${{ github.event_name == 'pull_request' && github.base_ref || github.ref_name }} + BranchParam: ${{ github.event_name == 'pull_request' && 'sonar.pullrequest.branch' || 'sonar.branch.name' }} + PullRequestParam: ${{ github.event_name == 'pull_request' && format('/d:sonar.pullrequest.key={0}', github.event.number) || '' }} steps: - name: Checkout @@ -43,6 +51,10 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DotnetVersion }} + - name: Install SonarScanner + run: dotnet tool install --global dotnet-sonarscanner + - name: Install Coverlet console + run: dotnet tool install --global coverlet.console - name: Install DocFX run: dotnet tool install --global docfx # See https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md @@ -60,13 +72,39 @@ jobs: - name: Build the solution run: dotnet build -c ${{ env.Configuration }} - - name: Run .NET tests + - name: Start SonarScanner + run: > + dotnet sonarscanner begin + /k:${{ env.SonarCloudProject }} + /v:GitHub_build_${{ env.RunNumber }} + /o:${{ env.SonarCloudUsername }} + /d:sonar.host.url=${{ env.SonarCloudUrl }} + /d:sonar.token=${{ env.SonarCloudSecretKey }} + /d:${{ env.BranchParam }}=${{ env.BranchName }} ${{ env.PullRequestParam }} + /d:sonar.javascript.lcov.reportPaths=$PWD/CSF.Screenplay.JsonToHtmlReport.Template/src/TestResults/lcov.info + /s:$PWD/.sonarqube-analysisproperties.xml + - name: Build the solution + run: dotnet build -c ${{ env.Configuration }} --no-incremental + - name: Run .NET tests with coverage id: dotnet_tests + shell: bash {0} + run: | + coverlet CSF.Extensions.WebDriver.Tests/bin/$Configuration/$Tfm/CSF.Extensions.WebDriver.Tests.dll --target "dotnet" --targetargs "test CSF.Extensions.WebDriver.Tests -c $Configuration --no-build --logger:nunit --test-adapter-path:." -f=opencover -o="TestResults/CSF.Extensions.WebDriver.Tests.opencover.xml" + exitCode=$? + if [ $exitCode -ne 0 ] + then + echo "One or more tests have failed; this build should eventually fail" + echo "failures=true" >> "$GITHUB_OUTPUT" + fi + - name: Run .NET tests run: dotnet test continue-on-error: true # Post-test tasks (artifacts, overall status) + - name: Stop SonarScanner + run: + dotnet sonarscanner end /d:sonar.token=${{ env.SonarCloudSecretKey }} - name: Gracefully stop Xvfb run: killall Xvfb continue-on-error: true @@ -76,7 +114,7 @@ jobs: name: NUnit test results path: Tests/*.Tests/**/TestResults.xml - name: Fail the build if any test failures - if: steps.dotnet_tests.outcome == 'failure' + if: steps.dotnet_tests.outputs.failures == 'failure' run: | echo "Failing the build due to test failures" exit 1 From 4c55e84258deeaa882ffe968f442a9bc8b43de3f Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 21 May 2026 18:11:26 +0100 Subject: [PATCH 05/19] Add SonarQube analysis file --- .sonarqube-analysisproperties.xml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .sonarqube-analysisproperties.xml diff --git a/.sonarqube-analysisproperties.xml b/.sonarqube-analysisproperties.xml new file mode 100644 index 0000000..56e3d49 --- /dev/null +++ b/.sonarqube-analysisproperties.xml @@ -0,0 +1,13 @@ + + + CSF.Extensions.WebDriver.Tests\**\*,**\*Exception.cs + docs\**\* + CSF.Extensions.WebDriver.Tests\**\* + **\TestResults.xml + TestResults\*.opencover.xml + false + CSF.Extensions.WebDriver.Tests\**\* + true + \ No newline at end of file From 7bbe6471a94d7740ada8e6428daf5c767df1c67c Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 21 May 2026 18:21:40 +0100 Subject: [PATCH 06/19] Attempt to fix test results path --- .github/workflows/dotnetCi.yml | 3 +-- .sonarqube-analysisproperties.xml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dotnetCi.yml b/.github/workflows/dotnetCi.yml index 4f8811b..0134396 100644 --- a/.github/workflows/dotnetCi.yml +++ b/.github/workflows/dotnetCi.yml @@ -81,7 +81,6 @@ jobs: /d:sonar.host.url=${{ env.SonarCloudUrl }} /d:sonar.token=${{ env.SonarCloudSecretKey }} /d:${{ env.BranchParam }}=${{ env.BranchName }} ${{ env.PullRequestParam }} - /d:sonar.javascript.lcov.reportPaths=$PWD/CSF.Screenplay.JsonToHtmlReport.Template/src/TestResults/lcov.info /s:$PWD/.sonarqube-analysisproperties.xml - name: Build the solution run: dotnet build -c ${{ env.Configuration }} --no-incremental @@ -112,7 +111,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: NUnit test results - path: Tests/*.Tests/**/TestResults.xml + path: '**/TestResults/*.xml' - name: Fail the build if any test failures if: steps.dotnet_tests.outputs.failures == 'failure' run: | diff --git a/.sonarqube-analysisproperties.xml b/.sonarqube-analysisproperties.xml index 56e3d49..dc90498 100644 --- a/.sonarqube-analysisproperties.xml +++ b/.sonarqube-analysisproperties.xml @@ -6,7 +6,7 @@ docs\**\* CSF.Extensions.WebDriver.Tests\**\* **\TestResults.xml - TestResults\*.opencover.xml + **\TestResults\*.opencover.xml false CSF.Extensions.WebDriver.Tests\**\* true From 87e7e71995cfdd86f7cb41e75ab5182392cf18ae Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 21 May 2026 18:22:47 +0100 Subject: [PATCH 07/19] Fix 2 tech issues --- .../ServiceCollectionExtensions.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs b/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs index c3a8c1e..4f02e1d 100644 --- a/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs +++ b/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs @@ -263,18 +263,16 @@ static Func s.ServiceType == typeof(ILoggerFactory))) - return services; + return; services.AddTransient(); services.AddTransient(typeof(ILogger<>), typeof(NullLogger<>)); - - return services; } - static IServiceCollection AddDriverOptionsFactory(IServiceCollection services) + static void AddDriverOptionsFactory(IServiceCollection services) { services.AddTransient(); services.AddTransient(); @@ -288,8 +286,6 @@ static IServiceCollection AddDriverOptionsFactory(IServiceCollection services) return service; }); - - return services; } } } From 8e8ca6d68de6f500bfa131b2ee3617f154d306ab Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 21 May 2026 18:23:53 +0100 Subject: [PATCH 08/19] Fix a tech issue --- CSF.Extensions.WebDriver/Factories/OptionsTypeProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CSF.Extensions.WebDriver/Factories/OptionsTypeProvider.cs b/CSF.Extensions.WebDriver/Factories/OptionsTypeProvider.cs index a38e047..afb2127 100644 --- a/CSF.Extensions.WebDriver/Factories/OptionsTypeProvider.cs +++ b/CSF.Extensions.WebDriver/Factories/OptionsTypeProvider.cs @@ -33,7 +33,7 @@ public bool TryGetOptionsType(WebDriverCreationOptions options, IConfigurationSe nameof(IWebDriver), driverType?.Name, nameof(WebDriverCreationOptions.OptionsType), - options?.OptionsType, + options.OptionsType, configuration.Key); return options.DriverFactoryType != null; From ace720d59d55df106fa78aa7a4a49283dafb1bba Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 21 May 2026 18:39:30 +0100 Subject: [PATCH 09/19] Attempt to fix test result logging --- .github/workflows/dotnetCi.yml | 4 +++- .sonarqube-analysisproperties.xml | 4 ++-- CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs | 6 ++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/dotnetCi.yml b/.github/workflows/dotnetCi.yml index 0134396..77d36b4 100644 --- a/.github/workflows/dotnetCi.yml +++ b/.github/workflows/dotnetCi.yml @@ -111,7 +111,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: NUnit test results - path: '**/TestResults/*.xml' + path: | + '**/TestResults/*.xml' + '**/*.TestResults.xml' - name: Fail the build if any test failures if: steps.dotnet_tests.outputs.failures == 'failure' run: | diff --git a/.sonarqube-analysisproperties.xml b/.sonarqube-analysisproperties.xml index dc90498..3b51a90 100644 --- a/.sonarqube-analysisproperties.xml +++ b/.sonarqube-analysisproperties.xml @@ -5,8 +5,8 @@ CSF.Extensions.WebDriver.Tests\**\*,**\*Exception.cs docs\**\* CSF.Extensions.WebDriver.Tests\**\* - **\TestResults.xml - **\TestResults\*.opencover.xml + **\*.TestResults.xml + TestResults\*.opencover.xml false CSF.Extensions.WebDriver.Tests\**\* true diff --git a/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs b/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs index 4f02e1d..36334a9 100644 --- a/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs +++ b/CSF.Extensions.WebDriver/ServiceCollectionExtensions.cs @@ -117,8 +117,8 @@ public static IServiceCollection AddWebDriverFactory(this IServiceCollection ser return services; } - static IServiceCollection AddWebDriverFactory(this IServiceCollection services, - Action configureOptions = null) + static void AddWebDriverFactory(this IServiceCollection services, + Action configureOptions = null) { AddWebDriverFactoryWithoutOptionsPattern(services); @@ -129,8 +129,6 @@ static IServiceCollection AddWebDriverFactory(this IServiceCollection services, AddDriverOptionsFactory(services); services.AddOptions().Configure(configureOptions ?? (o => {})); services.AddTransient(); - - return services; } /// From 612ee6d07558b436d8ebb889bcca66f871f8fe19 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 21 May 2026 19:07:48 +0100 Subject: [PATCH 10/19] Attempts to fix coverage and logging --- .github/workflows/dotnetCi.yml | 6 ++---- .gitignore | 2 +- .sonarqube-analysisproperties.xml | 4 ++-- .../CSF.Extensions.WebDriver.Tests.csproj | 15 +++++++++------ .../WebDriverProxyFactoryIntegrationTests.cs | 13 ++++++++++--- .../TestLoggerAttribute.cs | 2 +- 6 files changed, 25 insertions(+), 17 deletions(-) diff --git a/.github/workflows/dotnetCi.yml b/.github/workflows/dotnetCi.yml index 77d36b4..8f3451f 100644 --- a/.github/workflows/dotnetCi.yml +++ b/.github/workflows/dotnetCi.yml @@ -88,7 +88,7 @@ jobs: id: dotnet_tests shell: bash {0} run: | - coverlet CSF.Extensions.WebDriver.Tests/bin/$Configuration/$Tfm/CSF.Extensions.WebDriver.Tests.dll --target "dotnet" --targetargs "test CSF.Extensions.WebDriver.Tests -c $Configuration --no-build --logger:nunit --test-adapter-path:." -f=opencover -o="TestResults/CSF.Extensions.WebDriver.Tests.opencover.xml" + coverlet CSF.Extensions.WebDriver.Tests/bin/$Configuration/$Tfm/CSF.Extensions.WebDriver.Tests.dll --target "dotnet" --targetargs "test CSF.Extensions.WebDriver.Tests -c $Configuration --no-build --logger:nunit --test-adapter-path:." -f=opencover -o="CSF.Extensions.WebDriver.Tests/TestResults/CSF.Extensions.WebDriver.Tests.opencover.xml" exitCode=$? if [ $exitCode -ne 0 ] then @@ -111,9 +111,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: NUnit test results - path: | - '**/TestResults/*.xml' - '**/*.TestResults.xml' + path: '**/TestResults/*.xml' - name: Fail the build if any test failures if: steps.dotnet_tests.outputs.failures == 'failure' run: | diff --git a/.gitignore b/.gitignore index 83e31d9..d59afba 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ bin/ obj/ *.userprefs *.csproj.user -TestResult.xml +**/TestResults/** *.log *.orig *.nupkg diff --git a/.sonarqube-analysisproperties.xml b/.sonarqube-analysisproperties.xml index 3b51a90..dc90498 100644 --- a/.sonarqube-analysisproperties.xml +++ b/.sonarqube-analysisproperties.xml @@ -5,8 +5,8 @@ CSF.Extensions.WebDriver.Tests\**\*,**\*Exception.cs docs\**\* CSF.Extensions.WebDriver.Tests\**\* - **\*.TestResults.xml - TestResults\*.opencover.xml + **\TestResults.xml + **\TestResults\*.opencover.xml false CSF.Extensions.WebDriver.Tests\**\* true diff --git a/CSF.Extensions.WebDriver.Tests/CSF.Extensions.WebDriver.Tests.csproj b/CSF.Extensions.WebDriver.Tests/CSF.Extensions.WebDriver.Tests.csproj index 3231f5e..9f4868f 100644 --- a/CSF.Extensions.WebDriver.Tests/CSF.Extensions.WebDriver.Tests.csproj +++ b/CSF.Extensions.WebDriver.Tests/CSF.Extensions.WebDriver.Tests.csproj @@ -10,12 +10,15 @@ - - - - - - + + + + + all + runtime; build; native; contentfiles; analyzers + + + diff --git a/CSF.Extensions.WebDriver.Tests/Proxies/WebDriverProxyFactoryIntegrationTests.cs b/CSF.Extensions.WebDriver.Tests/Proxies/WebDriverProxyFactoryIntegrationTests.cs index 4ac9f4e..56770b6 100644 --- a/CSF.Extensions.WebDriver.Tests/Proxies/WebDriverProxyFactoryIntegrationTests.cs +++ b/CSF.Extensions.WebDriver.Tests/Proxies/WebDriverProxyFactoryIntegrationTests.cs @@ -9,7 +9,7 @@ namespace CSF.Extensions.WebDriver.Proxies; [TestFixture, Parallelizable, Description("Integration tests for IGetsProxyWebDriver and all of its dependencies. These tests use DI.")] public class WebDriverProxyFactoryIntegrationTests { - readonly IServiceProvider services; + IServiceProvider services; [Test,AutoMoqData] public void GetProxyWebDriverShouldGetAWebDriverWhichCanBeUnproxied(IWebDriver webDriver) @@ -76,8 +76,8 @@ public void GetProxyWebDriverShouldReturnAnObjectWhichHasQuirksFromStaticDataWhe }); } - - public WebDriverProxyFactoryIntegrationTests() + [OneTimeSetUp] + public void Setup() { var serviceCollection = new ServiceCollection(); serviceCollection @@ -98,4 +98,11 @@ public WebDriverProxyFactoryIntegrationTests() }}); services = serviceCollection.BuildServiceProvider(); } + + [OneTimeTearDown] + public void Teardown() + { + if(services is IDisposable disp) + disp.Dispose(); + } } \ No newline at end of file diff --git a/CSF.Extensions.WebDriver.Tests/TestLoggerAttribute.cs b/CSF.Extensions.WebDriver.Tests/TestLoggerAttribute.cs index b57659b..2a9eb16 100644 --- a/CSF.Extensions.WebDriver.Tests/TestLoggerAttribute.cs +++ b/CSF.Extensions.WebDriver.Tests/TestLoggerAttribute.cs @@ -58,6 +58,6 @@ public class TestContextLogger : ILogger public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - TestContext.WriteLine($"{logLevel}: {formatter(state, exception)}{((exception is not null) ? ("\n" + exception.ToString()) : string.Empty)}"); + TestContext.Out.WriteLine($"{logLevel}: {formatter(state, exception)}{((exception is not null) ? ("\n" + exception.ToString()) : string.Empty)}"); } } \ No newline at end of file From 57b858df7f7422ee15671dbcf7b0d235798d0178 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 21 May 2026 19:21:29 +0100 Subject: [PATCH 11/19] Remove dupe test --- .github/workflows/dotnetCi.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/dotnetCi.yml b/.github/workflows/dotnetCi.yml index 8f3451f..1438f89 100644 --- a/.github/workflows/dotnetCi.yml +++ b/.github/workflows/dotnetCi.yml @@ -95,9 +95,6 @@ jobs: echo "One or more tests have failed; this build should eventually fail" echo "failures=true" >> "$GITHUB_OUTPUT" fi - - name: Run .NET tests - run: dotnet test - continue-on-error: true # Post-test tasks (artifacts, overall status) From fadf6fbabac9849929ea2f8ac96ab3a9458d9d25 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 21 May 2026 19:34:14 +0100 Subject: [PATCH 12/19] Attempts to fix coverage and logging --- .github/workflows/dotnetCi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnetCi.yml b/.github/workflows/dotnetCi.yml index 1438f89..f0081ef 100644 --- a/.github/workflows/dotnetCi.yml +++ b/.github/workflows/dotnetCi.yml @@ -88,7 +88,7 @@ jobs: id: dotnet_tests shell: bash {0} run: | - coverlet CSF.Extensions.WebDriver.Tests/bin/$Configuration/$Tfm/CSF.Extensions.WebDriver.Tests.dll --target "dotnet" --targetargs "test CSF.Extensions.WebDriver.Tests -c $Configuration --no-build --logger:nunit --test-adapter-path:." -f=opencover -o="CSF.Extensions.WebDriver.Tests/TestResults/CSF.Extensions.WebDriver.Tests.opencover.xml" + coverlet CSF.Extensions.WebDriver.Tests/bin/$Configuration/$Tfm/CSF.Extensions.WebDriver.Tests.dll --target "dotnet" --targetargs "test -c $Configuration --no-build --logger:nunit --test-adapter-path:." -f opencover -o "CSF.Extensions.WebDriver.Tests/TestResults/CSF.Extensions.WebDriver.Tests.opencover.xml" exitCode=$? if [ $exitCode -ne 0 ] then From 728510c66f6dcd3643478a53c81cbbbc57497539 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 21 May 2026 19:40:06 +0100 Subject: [PATCH 13/19] Remove redundant build step --- .github/workflows/dotnetCi.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/dotnetCi.yml b/.github/workflows/dotnetCi.yml index f0081ef..6b6910f 100644 --- a/.github/workflows/dotnetCi.yml +++ b/.github/workflows/dotnetCi.yml @@ -70,8 +70,6 @@ jobs: # Build and test the solution - - name: Build the solution - run: dotnet build -c ${{ env.Configuration }} - name: Start SonarScanner run: > dotnet sonarscanner begin From 337c34e37eef331adecc9d415db72824260903d0 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 21 May 2026 19:46:38 +0100 Subject: [PATCH 14/19] Downgrade coverlet This is the version which works with Screenplay --- .github/workflows/dotnetCi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnetCi.yml b/.github/workflows/dotnetCi.yml index 6b6910f..7e3d07e 100644 --- a/.github/workflows/dotnetCi.yml +++ b/.github/workflows/dotnetCi.yml @@ -54,7 +54,7 @@ jobs: - name: Install SonarScanner run: dotnet tool install --global dotnet-sonarscanner - name: Install Coverlet console - run: dotnet tool install --global coverlet.console + run: dotnet tool install --global coverlet.console --version 8.0.1 - name: Install DocFX run: dotnet tool install --global docfx # See https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md From b2f64b21e1849f3458632ee600d42b1727e4cd8b Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 21 May 2026 19:50:59 +0100 Subject: [PATCH 15/19] Fix missing tfm --- .github/workflows/dotnetCi.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dotnetCi.yml b/.github/workflows/dotnetCi.yml index 7e3d07e..fee0793 100644 --- a/.github/workflows/dotnetCi.yml +++ b/.github/workflows/dotnetCi.yml @@ -31,6 +31,7 @@ jobs: SonarCloudUrl: https://sonarcloud.io SonarCloudSecretKey: ${{ secrets.SONARCLOUDKEY }} Configuration: Debug + Tfm: net8.0 DotnetVersion: 8.0.x DISPLAY: :99 BranchName: ${{ github.event_name == 'pull_request' && github.base_ref || github.ref_name }} From ceb7b00ad980107418b82bfbc2798e90fbd70170 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 21 May 2026 20:12:34 +0100 Subject: [PATCH 16/19] Resolve #59 - Add tests for setting log levels --- ...LevelDriverOptionsFactoryDecoratorTests.cs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 CSF.Extensions.WebDriver.Tests/Factories/LogLevelDriverOptionsFactoryDecoratorTests.cs diff --git a/CSF.Extensions.WebDriver.Tests/Factories/LogLevelDriverOptionsFactoryDecoratorTests.cs b/CSF.Extensions.WebDriver.Tests/Factories/LogLevelDriverOptionsFactoryDecoratorTests.cs new file mode 100644 index 0000000..070dd2c --- /dev/null +++ b/CSF.Extensions.WebDriver.Tests/Factories/LogLevelDriverOptionsFactoryDecoratorTests.cs @@ -0,0 +1,67 @@ +using Microsoft.Extensions.Configuration; +using NUnit.Framework.Internal; +using OpenQA.Selenium; + +namespace CSF.Extensions.WebDriver.Factories; + +[TestFixture, Parallelizable] +public class LogLevelDriverOptionsFactoryDecoratorTests +{ + [Test, AutoMoqData] + public void CreateOptionsShouldSetLogLevelIfSpecifiedInConfig([Frozen] ICreatesDriverOptions wrapped, + LogLevelDriverOptionsFactoryDecorator sut, + Type optionsType, + IConfigurationSection config) + { + var options = new InspectableDriverOptions(); + Mock.Get(wrapped).Setup(x => x.CreateOptions(optionsType, config)).Returns(options); + Mock.Get(config) + .Setup(x => x.GetSection(nameof(WebDriverCreationOptions.BrowserLogLevel))) + .Returns(Mock.Of(x => x.Value == LogLevel.Severe.ToString())); + + sut.CreateOptions(optionsType, config); + + Assert.That(() => options.GetLoggingPrefs()[LogType.Browser], Is.EqualTo("SEVERE")); + } + + [Test, AutoMoqData] + public void CreateOptionsShouldNotSetLogLevelIfOmittedInConfig([Frozen] ICreatesDriverOptions wrapped, + LogLevelDriverOptionsFactoryDecorator sut, + Type optionsType, + IConfigurationSection config) + { + var options = new InspectableDriverOptions(); + Mock.Get(wrapped).Setup(x => x.CreateOptions(optionsType, config)).Returns(options); + Mock.Get(config) + .Setup(x => x.GetSection(nameof(WebDriverCreationOptions.BrowserLogLevel))) + .Returns(Mock.Of(x => x.Value == null)); + + sut.CreateOptions(optionsType, config); + + Assert.That(() => options.GetLoggingPrefs(), Is.Null); + } + + [Test, AutoMoqData] + public void CreateOptionsShouldNotSetLogLevelIfConfigIsInvalid([Frozen] ICreatesDriverOptions wrapped, + LogLevelDriverOptionsFactoryDecorator sut, + Type optionsType, + IConfigurationSection config) + { + var options = new InspectableDriverOptions(); + Mock.Get(wrapped).Setup(x => x.CreateOptions(optionsType, config)).Returns(options); + Mock.Get(config) + .Setup(x => x.GetSection(nameof(WebDriverCreationOptions.BrowserLogLevel))) + .Returns(Mock.Of(x => x.Value == "invalid level")); + + sut.CreateOptions(optionsType, config); + + Assert.That(() => options.GetLoggingPrefs(), Is.Null); + } + + class InspectableDriverOptions : DriverOptions + { + public Dictionary GetLoggingPrefs() => GenerateLoggingPreferencesDictionary(); + + public override ICapabilities ToCapabilities() => throw new NotImplementedException(); + } +} \ No newline at end of file From c4beda470b1ae90bdac6c35547ec6148d268c47d Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 21 May 2026 20:20:10 +0100 Subject: [PATCH 17/19] Add docs website publishing workflow --- .github/workflows/publishDocsWebsite.yml | 47 ++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/publishDocsWebsite.yml diff --git a/.github/workflows/publishDocsWebsite.yml b/.github/workflows/publishDocsWebsite.yml new file mode 100644 index 0000000..48160c2 --- /dev/null +++ b/.github/workflows/publishDocsWebsite.yml @@ -0,0 +1,47 @@ +name: Update docs website + +on: + push: + branches: [ "master" ] + +jobs: + + # This job builds the documentation website + # and the commits that to the master branch, + # which will result in replacing the currently published site + + build_and_commit: + name: Build & commit docs website + runs-on: ubuntu-slim + + env: + DotnetVersion: 8.0.x + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Add .NET global tools location to PATH + run: echo "$HOME/.dotnet/tools" >> "$GITHUB_PATH" + - name: Install .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DotnetVersion }} + - name: Install DocFX + run: dotnet tool install --global docfx + - name: Remove old docs + run: dotnet clean CSF.Extensions.WebDriver.Docs + - name: Build new docs + working-directory: CSF.Extensions.WebDriver.Docs + run: docfx docfx.json + - name: Add & Commit + uses: EndBug/add-and-commit@v9.1.4 + with: + add: -A docs/ + default_author: github_actor + committer_name: Github Actions Workflow (bot) + committer_email: github-actions-workflow@bots.noreply.github.com + fetch: true + message: Publish updated documentation website + push: origin master + + From 99d3494ed67c9e0dea049244de54bfc3af3f5f92 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 21 May 2026 20:21:10 +0100 Subject: [PATCH 18/19] Fix removing docs --- .github/workflows/publishDocsWebsite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publishDocsWebsite.yml b/.github/workflows/publishDocsWebsite.yml index 48160c2..45df5ee 100644 --- a/.github/workflows/publishDocsWebsite.yml +++ b/.github/workflows/publishDocsWebsite.yml @@ -29,7 +29,7 @@ jobs: - name: Install DocFX run: dotnet tool install --global docfx - name: Remove old docs - run: dotnet clean CSF.Extensions.WebDriver.Docs + run: rm -rf docs/* - name: Build new docs working-directory: CSF.Extensions.WebDriver.Docs run: docfx docfx.json From fceb418ffc3e1fab4483ca33e4f6a5b70e97d306 Mon Sep 17 00:00:00 2001 From: Craig Fowler Date: Thu, 21 May 2026 20:23:11 +0100 Subject: [PATCH 19/19] Fix minor tech issue --- .../Factories/LogLevelDriverOptionsFactoryDecoratorTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CSF.Extensions.WebDriver.Tests/Factories/LogLevelDriverOptionsFactoryDecoratorTests.cs b/CSF.Extensions.WebDriver.Tests/Factories/LogLevelDriverOptionsFactoryDecoratorTests.cs index 070dd2c..f61acd4 100644 --- a/CSF.Extensions.WebDriver.Tests/Factories/LogLevelDriverOptionsFactoryDecoratorTests.cs +++ b/CSF.Extensions.WebDriver.Tests/Factories/LogLevelDriverOptionsFactoryDecoratorTests.cs @@ -21,7 +21,7 @@ public void CreateOptionsShouldSetLogLevelIfSpecifiedInConfig([Frozen] ICreatesD sut.CreateOptions(optionsType, config); - Assert.That(() => options.GetLoggingPrefs()[LogType.Browser], Is.EqualTo("SEVERE")); + Assert.That(options.GetLoggingPrefs()[LogType.Browser], Is.EqualTo("SEVERE")); } [Test, AutoMoqData] @@ -38,7 +38,7 @@ public void CreateOptionsShouldNotSetLogLevelIfOmittedInConfig([Frozen] ICreates sut.CreateOptions(optionsType, config); - Assert.That(() => options.GetLoggingPrefs(), Is.Null); + Assert.That(options.GetLoggingPrefs(), Is.Null); } [Test, AutoMoqData] @@ -55,7 +55,7 @@ public void CreateOptionsShouldNotSetLogLevelIfConfigIsInvalid([Frozen] ICreates sut.CreateOptions(optionsType, config); - Assert.That(() => options.GetLoggingPrefs(), Is.Null); + Assert.That(options.GetLoggingPrefs(), Is.Null); } class InspectableDriverOptions : DriverOptions