diff --git a/dotnet/src/webdriver/BiDi/BiDi.cs b/dotnet/src/webdriver/BiDi/BiDi.cs index 510dab74dd180..b65b15078cbd1 100644 --- a/dotnet/src/webdriver/BiDi/BiDi.cs +++ b/dotnet/src/webdriver/BiDi/BiDi.cs @@ -20,65 +20,62 @@ using System.Collections.Concurrent; using System.Text.Json; using System.Text.Json.Serialization; -using OpenQA.Selenium.BiDi.Json.Converters; +using OpenQA.Selenium.BiDi.Session; namespace OpenQA.Selenium.BiDi; -public sealed class BiDi : IAsyncDisposable +public sealed class BiDi : IBiDi { private readonly ConcurrentDictionary _modules = new(); - private BiDi(string url) - { - var uri = new Uri(url); - - Broker = new Broker(this, uri); - } + private Broker Broker { get; set; } = null!; - private Broker Broker { get; } + internal ISessionModule Session => AsModule(); - internal Session.SessionModule SessionModule => AsModule(); + private BiDi() { } - public BrowsingContext.BrowsingContextModule BrowsingContext => AsModule(); + public BrowsingContext.IBrowsingContextModule BrowsingContext => AsModule(); - public Browser.BrowserModule Browser => AsModule(); + public Browser.IBrowserModule Browser => AsModule(); - public Network.NetworkModule Network => AsModule(); + public Network.INetworkModule Network => AsModule(); - public Input.InputModule Input => AsModule(); + public Input.IInputModule Input => AsModule(); - public Script.ScriptModule Script => AsModule(); + public Script.IScriptModule Script => AsModule(); - public Log.LogModule Log => AsModule(); + public Log.ILogModule Log => AsModule(); - public Storage.StorageModule Storage => AsModule(); + public Storage.IStorageModule Storage => AsModule(); - public WebExtension.WebExtensionModule WebExtension => AsModule(); + public WebExtension.IWebExtensionModule WebExtension => AsModule(); - public Emulation.EmulationModule Emulation => AsModule(); + public Emulation.IEmulationModule Emulation => AsModule(); - public static async Task ConnectAsync(string url, BiDiOptions? options = null, CancellationToken cancellationToken = default) + public static async Task ConnectAsync(string url, BiDiOptions? options = null, CancellationToken cancellationToken = default) { - var bidi = new BiDi(url); + var transport = await WebSocketTransport.ConnectAsync(new Uri(url), cancellationToken).ConfigureAwait(false); + + BiDi bidi = new(); - await bidi.Broker.ConnectAsync(cancellationToken).ConfigureAwait(false); + bidi.Broker = new Broker(transport, bidi, () => bidi.Session); return bidi; } - public Task StatusAsync(Session.StatusOptions? options = null, CancellationToken cancellationToken = default) + public Task StatusAsync(StatusOptions? options = null, CancellationToken cancellationToken = default) { - return SessionModule.StatusAsync(options, cancellationToken); + return Session.StatusAsync(options, cancellationToken); } - public Task NewAsync(Session.CapabilitiesRequest capabilities, Session.NewOptions? options = null, CancellationToken cancellationToken = default) + public Task NewAsync(CapabilitiesRequest capabilities, NewOptions? options = null, CancellationToken cancellationToken = default) { - return SessionModule.NewAsync(capabilities, options, cancellationToken); + return Session.NewAsync(capabilities, options, cancellationToken); } - public Task EndAsync(Session.EndOptions? options = null, CancellationToken cancellationToken = default) + public Task EndAsync(EndOptions? options = null, CancellationToken cancellationToken = default) { - return SessionModule.EndAsync(options, cancellationToken); + return Session.EndAsync(options, cancellationToken); } public async ValueTask DisposeAsync() @@ -101,7 +98,7 @@ private static JsonSerializerOptions CreateDefaultJsonOptions() DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Converters = { - new DateTimeOffsetConverter(), + new Json.Converters.DateTimeOffsetConverter(), } }; } diff --git a/dotnet/src/webdriver/BiDi/Broker.cs b/dotnet/src/webdriver/BiDi/Broker.cs index 8e3b8ec8c1e5d..5f5477f010276 100644 --- a/dotnet/src/webdriver/BiDi/Broker.cs +++ b/dotnet/src/webdriver/BiDi/Broker.cs @@ -20,7 +20,7 @@ using System.Collections.Concurrent; using System.Text.Json; using System.Text.Json.Serialization.Metadata; -using System.Threading.Channels; +using OpenQA.Selenium.BiDi.Session; using OpenQA.Selenium.Internal.Logging; namespace OpenQA.Selenium.BiDi; @@ -29,107 +29,33 @@ internal sealed class Broker : IAsyncDisposable { private readonly ILogger _logger = Internal.Logging.Log.GetLogger(); - private readonly BiDi _bidi; private readonly ITransport _transport; + private readonly EventDispatcher _eventDispatcher; + private readonly IBiDi _bidi; private readonly ConcurrentDictionary _pendingCommands = new(); - private readonly Channel _pendingEvents = Channel.CreateUnbounded(new() - { - SingleReader = true, - SingleWriter = true - }); - private readonly Dictionary _eventTypesMap = []; - - private readonly ConcurrentDictionary> _eventHandlers = new(); private long _currentCommandId; private static readonly TaskFactory _myTaskFactory = new(CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskContinuationOptions.None, TaskScheduler.Default); - private Task? _receivingMessageTask; - private Task? _eventEmitterTask; - private CancellationTokenSource? _receiveMessagesCancellationTokenSource; + private readonly Task _receivingMessageTask; + private readonly CancellationTokenSource _receiveMessagesCancellationTokenSource; - internal Broker(BiDi bidi, Uri url) + public Broker(ITransport transport, IBiDi bidi, Func sessionProvider) { + _transport = transport; _bidi = bidi; - _transport = new WebSocketTransport(url); - } - - public async Task ConnectAsync(CancellationToken cancellationToken) - { - await _transport.ConnectAsync(cancellationToken).ConfigureAwait(false); + _eventDispatcher = new EventDispatcher(sessionProvider); _receiveMessagesCancellationTokenSource = new CancellationTokenSource(); _receivingMessageTask = _myTaskFactory.StartNew(async () => await ReceiveMessagesAsync(_receiveMessagesCancellationTokenSource.Token), TaskCreationOptions.LongRunning).Unwrap(); - _eventEmitterTask = _myTaskFactory.StartNew(ProcessEventsAwaiterAsync).Unwrap(); } - private async Task ReceiveMessagesAsync(CancellationToken cancellationToken) - { - try - { - while (!cancellationToken.IsCancellationRequested) - { - var data = await _transport.ReceiveAsync(cancellationToken).ConfigureAwait(false); - - try - { - ProcessReceivedMessage(data); - } - catch (Exception ex) - { - if (_logger.IsEnabled(LogEventLevel.Error)) - { - _logger.Error($"Unhandled error occurred while processing remote message: {ex}"); - } - } - } - } - catch (Exception ex) when (ex is not OperationCanceledException) - { - if (_logger.IsEnabled(LogEventLevel.Error)) - { - _logger.Error($"Unhandled error occurred while receiving remote messages: {ex}"); - } - - throw; - } - } - - private async Task ProcessEventsAwaiterAsync() + public Task SubscribeAsync(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken) + where TEventArgs : EventArgs { - var reader = _pendingEvents.Reader; - while (await reader.WaitToReadAsync().ConfigureAwait(false)) - { - while (reader.TryRead(out var result)) - { - try - { - if (_eventHandlers.TryGetValue(result.Method, out var eventHandlers)) - { - if (eventHandlers is not null) - { - foreach (var handler in eventHandlers.ToArray()) // copy handlers avoiding modified collection while iterating - { - var args = result.Params; - - args.BiDi = _bidi; - - await handler.InvokeAsync(args).ConfigureAwait(false); - } - } - } - } - catch (Exception ex) - { - if (_logger.IsEnabled(LogEventLevel.Error)) - { - _logger.Error($"Unhandled error processing BiDi event handler: {ex}"); - } - } - } - } + return _eventDispatcher.SubscribeAsync(eventName, eventHandler, options, jsonTypeInfo, cancellationToken); } public async Task ExecuteCommandAsync(TCommand command, CommandOptions? options, JsonTypeInfo jsonCommandTypeInfo, JsonTypeInfo jsonResultTypeInfo, CancellationToken cancellationToken) @@ -157,40 +83,23 @@ public async Task ExecuteCommandAsync(TCommand comma return (TResult)await tcs.Task.ConfigureAwait(false); } - public async Task SubscribeAsync(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken) - where TEventArgs : EventArgs - { - _eventTypesMap[eventName] = jsonTypeInfo; - - var handlers = _eventHandlers.GetOrAdd(eventName, (a) => []); - - var subscribeResult = await _bidi.SessionModule.SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken).ConfigureAwait(false); - - handlers.Add(eventHandler); - - return new Subscription(subscribeResult.Subscription, this, eventHandler); - } - - public async Task UnsubscribeAsync(Subscription subscription, CancellationToken cancellationToken) - { - var eventHandlers = _eventHandlers[subscription.EventHandler.EventName]; - - eventHandlers.Remove(subscription.EventHandler); - - await _bidi.SessionModule.UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false); - } - public async ValueTask DisposeAsync() { - _pendingEvents.Writer.Complete(); + _receiveMessagesCancellationTokenSource.Cancel(); - _receiveMessagesCancellationTokenSource?.Cancel(); + await _eventDispatcher.DisposeAsync().ConfigureAwait(false); - if (_eventEmitterTask is not null) + try + { + await _receivingMessageTask.ConfigureAwait(false); + } + catch (OperationCanceledException) when (_receiveMessagesCancellationTokenSource.IsCancellationRequested) { - await _eventEmitterTask.ConfigureAwait(false); + // Expected when cancellation is requested, ignore. } + _receiveMessagesCancellationTokenSource.Dispose(); + _transport.Dispose(); GC.SuppressFinalize(this); @@ -204,7 +113,8 @@ private void ProcessReceivedMessage(byte[]? data) string? error = default; string? message = default; Utf8JsonReader resultReader = default; - Utf8JsonReader paramsReader = default; + long paramsStartIndex = 0; + long paramsEndIndex = 0; Utf8JsonReader reader = new(new ReadOnlySpan(data)); reader.Read(); @@ -235,7 +145,7 @@ private void ProcessReceivedMessage(byte[]? data) break; case "params": - paramsReader = reader; // snapshot + paramsStartIndex = reader.TokenStartIndex; break; case "error": @@ -247,21 +157,29 @@ private void ProcessReceivedMessage(byte[]? data) break; } - reader.Skip(); + if (propertyName == "params") + { + reader.Skip(); + paramsEndIndex = reader.BytesConsumed; + } + else + { + reader.Skip(); + } reader.Read(); } switch (type) { case "success": - if (id is null) throw new JsonException("The remote end responded with 'success' message type, but missed required 'id' property."); + if (id is null) throw new BiDiException("The remote end responded with 'success' message type, but missed required 'id' property."); if (_pendingCommands.TryGetValue(id.Value, out var command)) { try { var commandResult = JsonSerializer.Deserialize(ref resultReader, command.JsonResultTypeInfo) - ?? throw new JsonException("Remote end returned null command result in the 'result' property."); + ?? throw new BiDiException("Remote end returned null command result in the 'result' property."); command.TaskCompletionSource.SetResult((EmptyResult)commandResult); } @@ -282,25 +200,13 @@ private void ProcessReceivedMessage(byte[]? data) break; case "event": - if (method is null) throw new JsonException("The remote end responded with 'event' message type, but missed required 'method' property."); - - if (_eventTypesMap.TryGetValue(method, out var eventInfo)) - { - var eventArgs = (EventArgs)JsonSerializer.Deserialize(ref paramsReader, eventInfo)!; - - eventArgs.BiDi = _bidi; - - _pendingEvents.Writer.TryWrite(new EventInfo(method, eventArgs)); - } - else - { - throw new BiDiException($"The remote end responded with 'event' message type, but no event type mapping for method '{method}' was found."); - } - + if (method is null) throw new BiDiException("The remote end responded with 'event' message type, but missed required 'method' property."); + var paramsJsonData = new ReadOnlyMemory(data, (int)paramsStartIndex, (int)(paramsEndIndex - paramsStartIndex)); + _eventDispatcher.EnqueueEvent(method, paramsJsonData, _bidi); break; case "error": - if (id is null) throw new JsonException("The remote end responded with 'error' message type, but missed required 'id' property."); + if (id is null) throw new BiDiException("The remote end responded with 'error' message type, but missed required 'id' property."); if (_pendingCommands.TryGetValue(id.Value, out var errorCommand)) { @@ -316,7 +222,37 @@ private void ProcessReceivedMessage(byte[]? data) } } - private readonly record struct CommandInfo(TaskCompletionSource TaskCompletionSource, JsonTypeInfo JsonResultTypeInfo); + private async Task ReceiveMessagesAsync(CancellationToken cancellationToken) + { + try + { + while (!cancellationToken.IsCancellationRequested) + { + var data = await _transport.ReceiveAsync(cancellationToken).ConfigureAwait(false); - private readonly record struct EventInfo(string Method, EventArgs Params); + try + { + ProcessReceivedMessage(data); + } + catch (Exception ex) + { + if (_logger.IsEnabled(LogEventLevel.Error)) + { + _logger.Error($"Unhandled error occurred while processing remote message: {ex}"); + } + } + } + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + if (_logger.IsEnabled(LogEventLevel.Error)) + { + _logger.Error($"Unhandled error occurred while receiving remote messages: {ex}"); + } + + throw; + } + } + + private readonly record struct CommandInfo(TaskCompletionSource TaskCompletionSource, JsonTypeInfo JsonResultTypeInfo); } diff --git a/dotnet/src/webdriver/BiDi/Browser/BrowserModule.cs b/dotnet/src/webdriver/BiDi/Browser/BrowserModule.cs index 19bce5cabdd77..4885e720a512c 100644 --- a/dotnet/src/webdriver/BiDi/Browser/BrowserModule.cs +++ b/dotnet/src/webdriver/BiDi/Browser/BrowserModule.cs @@ -23,7 +23,7 @@ namespace OpenQA.Selenium.BiDi.Browser; -public sealed class BrowserModule : Module +public sealed class BrowserModule : Module, IBrowserModule { private BrowserJsonSerializerContext _jsonContext = null!; @@ -77,7 +77,7 @@ public async Task SetDownloadBehaviorDeniedAsync(SetD return await ExecuteCommandAsync(new SetDownloadBehaviorCommand(@params), options, _jsonContext.SetDownloadBehaviorCommand, _jsonContext.SetDownloadBehaviorResult, cancellationToken).ConfigureAwait(false); } - protected override void Initialize(BiDi bidi, JsonSerializerOptions jsonSerializerOptions) + protected override void Initialize(IBiDi bidi, JsonSerializerOptions jsonSerializerOptions) { jsonSerializerOptions.Converters.Add(new BrowserUserContextConverter(bidi)); diff --git a/dotnet/src/webdriver/BiDi/Browser/IBrowserModule.cs b/dotnet/src/webdriver/BiDi/Browser/IBrowserModule.cs new file mode 100644 index 0000000000000..b79395b311e81 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Browser/IBrowserModule.cs @@ -0,0 +1,32 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +namespace OpenQA.Selenium.BiDi.Browser; + +public interface IBrowserModule +{ + Task CloseAsync(CloseOptions? options = null, CancellationToken cancellationToken = default); + Task CreateUserContextAsync(CreateUserContextOptions? options = null, CancellationToken cancellationToken = default); + Task GetClientWindowsAsync(GetClientWindowsOptions? options = null, CancellationToken cancellationToken = default); + Task GetUserContextsAsync(GetUserContextsOptions? options = null, CancellationToken cancellationToken = default); + Task RemoveUserContextAsync(UserContext userContext, RemoveUserContextOptions? options = null, CancellationToken cancellationToken = default); + Task SetDownloadBehaviorAllowedAsync(string destinationFolder, SetDownloadBehaviorOptions? options = null, CancellationToken cancellationToken = default); + Task SetDownloadBehaviorAllowedAsync(SetDownloadBehaviorOptions? options = null, CancellationToken cancellationToken = default); + Task SetDownloadBehaviorDeniedAsync(SetDownloadBehaviorOptions? options = null, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/webdriver/BiDi/Browser/UserContext.cs b/dotnet/src/webdriver/BiDi/Browser/UserContext.cs index 229c2de8c037a..9be2ce92e76b7 100644 --- a/dotnet/src/webdriver/BiDi/Browser/UserContext.cs +++ b/dotnet/src/webdriver/BiDi/Browser/UserContext.cs @@ -24,7 +24,7 @@ namespace OpenQA.Selenium.BiDi.Browser; public sealed record UserContext { - public UserContext(BiDi bidi, string id) + public UserContext(IBiDi bidi, string id) : this(id) { BiDi = bidi ?? throw new ArgumentNullException(nameof(bidi)); @@ -38,10 +38,10 @@ internal UserContext(string id) internal string Id { get; } - private BiDi? _bidi; + private IBiDi? _bidi; [JsonIgnore] - public BiDi BiDi + public IBiDi BiDi { get => _bidi ?? throw new InvalidOperationException($"{nameof(BiDi)} instance has not been hydrated."); internal set => _bidi = value; diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs index 22b049f24853f..04abcb9e11f67 100644 --- a/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs @@ -24,7 +24,7 @@ namespace OpenQA.Selenium.BiDi.BrowsingContext; public sealed record BrowsingContext { - public BrowsingContext(BiDi bidi, string id) + public BrowsingContext(IBiDi bidi, string id) : this(id) { BiDi = bidi ?? throw new ArgumentNullException(nameof(bidi)); @@ -36,37 +36,37 @@ internal BrowsingContext(string id) Id = id; } - private BrowsingContextLogModule? _logModule; - private BrowsingContextNetworkModule? _networkModule; - private BrowsingContextScriptModule? _scriptModule; - private BrowsingContextStorageModule? _storageModule; - private BrowsingContextInputModule? _inputModule; + private IBrowsingContextLogModule? _logModule; + private IBrowsingContextNetworkModule? _networkModule; + private IBrowsingContextScriptModule? _scriptModule; + private IBrowsingContextStorageModule? _storageModule; + private IBrowsingContextInputModule? _inputModule; internal string Id { get; } - private BiDi? _bidi; + private IBiDi? _bidi; [JsonIgnore] - public BiDi BiDi + public IBiDi BiDi { get => _bidi ?? throw new InvalidOperationException($"{nameof(BiDi)} instance has not been hydrated."); internal set => _bidi = value; } [JsonIgnore] - public BrowsingContextLogModule Log => _logModule ?? Interlocked.CompareExchange(ref _logModule, new BrowsingContextLogModule(this, BiDi.Log), null) ?? _logModule; + public IBrowsingContextLogModule Log => _logModule ?? Interlocked.CompareExchange(ref _logModule, new BrowsingContextLogModule(this, BiDi.Log), null) ?? _logModule; [JsonIgnore] - public BrowsingContextNetworkModule Network => _networkModule ?? Interlocked.CompareExchange(ref _networkModule, new BrowsingContextNetworkModule(this, BiDi.Network), null) ?? _networkModule; + public IBrowsingContextNetworkModule Network => _networkModule ?? Interlocked.CompareExchange(ref _networkModule, new BrowsingContextNetworkModule(this, BiDi.Network), null) ?? _networkModule; [JsonIgnore] - public BrowsingContextScriptModule Script => _scriptModule ?? Interlocked.CompareExchange(ref _scriptModule, new BrowsingContextScriptModule(this, BiDi.Script), null) ?? _scriptModule; + public IBrowsingContextScriptModule Script => _scriptModule ?? Interlocked.CompareExchange(ref _scriptModule, new BrowsingContextScriptModule(this, BiDi.Script), null) ?? _scriptModule; [JsonIgnore] - public BrowsingContextStorageModule Storage => _storageModule ?? Interlocked.CompareExchange(ref _storageModule, new BrowsingContextStorageModule(this, BiDi.Storage), null) ?? _storageModule; + public IBrowsingContextStorageModule Storage => _storageModule ?? Interlocked.CompareExchange(ref _storageModule, new BrowsingContextStorageModule(this, BiDi.Storage), null) ?? _storageModule; [JsonIgnore] - public BrowsingContextInputModule Input => _inputModule ?? Interlocked.CompareExchange(ref _inputModule, new BrowsingContextInputModule(this, BiDi.Input), null) ?? _inputModule; + public IBrowsingContextInputModule Input => _inputModule ?? Interlocked.CompareExchange(ref _inputModule, new BrowsingContextInputModule(this, BiDi.Input), null) ?? _inputModule; public Task NavigateAsync(string url, NavigateOptions? options = null, CancellationToken cancellationToken = default) { diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextInputModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextInputModule.cs index 0b7b9b526177a..7a10aa1c2f22c 100644 --- a/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextInputModule.cs +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextInputModule.cs @@ -21,7 +21,7 @@ namespace OpenQA.Selenium.BiDi.BrowsingContext; -public sealed class BrowsingContextInputModule(BrowsingContext context, InputModule inputModule) +public sealed class BrowsingContextInputModule(BrowsingContext context, IInputModule inputModule) : IBrowsingContextInputModule { public Task PerformActionsAsync(IEnumerable actions, PerformActionsOptions? options = null, CancellationToken cancellationToken = default) { diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextLogModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextLogModule.cs index e9adafc1a374b..20699843ecb92 100644 --- a/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextLogModule.cs +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextLogModule.cs @@ -21,7 +21,7 @@ namespace OpenQA.Selenium.BiDi.BrowsingContext; -public sealed class BrowsingContextLogModule(BrowsingContext context, LogModule logModule) +public sealed class BrowsingContextLogModule(BrowsingContext context, ILogModule logModule) : IBrowsingContextLogModule { public Task OnEntryAddedAsync(Func handler, ContextSubscriptionOptions? options = null, CancellationToken cancellationToken = default) { diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextModule.cs index a7119bd9df2f5..08b5bc39cbcdf 100644 --- a/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextModule.cs +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextModule.cs @@ -23,7 +23,7 @@ namespace OpenQA.Selenium.BiDi.BrowsingContext; -public sealed class BrowsingContextModule : Module +public sealed class BrowsingContextModule : Module, IBrowsingContextModule { private BrowsingContextJsonSerializerContext _jsonContext = null!; @@ -251,7 +251,7 @@ public async Task OnUserPromptClosedAsync(Action InterceptRequestAsync(Func handler, InterceptRequestOptions? options = null, CancellationToken cancellationToken = default) { diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextScriptModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextScriptModule.cs index 17b76e2880b7c..233e60bd09d33 100644 --- a/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextScriptModule.cs +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextScriptModule.cs @@ -22,7 +22,7 @@ namespace OpenQA.Selenium.BiDi.BrowsingContext; -public sealed class BrowsingContextScriptModule(BrowsingContext context, ScriptModule scriptModule) +public sealed class BrowsingContextScriptModule(BrowsingContext context, IScriptModule scriptModule) : IBrowsingContextScriptModule { public Task AddPreloadScriptAsync([StringSyntax(StringSyntaxConstants.JavaScript)] string functionDeclaration, ContextAddPreloadScriptOptions? options = null, CancellationToken cancellationToken = default) { diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextStorageModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextStorageModule.cs index d26770336c4d3..c88cd03ee9d09 100644 --- a/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextStorageModule.cs +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextStorageModule.cs @@ -21,7 +21,7 @@ namespace OpenQA.Selenium.BiDi.BrowsingContext; -public sealed class BrowsingContextStorageModule(BrowsingContext context, StorageModule storageModule) +public sealed class BrowsingContextStorageModule(BrowsingContext context, IStorageModule storageModule) : IBrowsingContextStorageModule { public Task GetCookiesAsync(ContextGetCookiesOptions? options = null, CancellationToken cancellationToken = default) { diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextInputModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextInputModule.cs new file mode 100644 index 0000000000000..ac76063ca471c --- /dev/null +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextInputModule.cs @@ -0,0 +1,31 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using OpenQA.Selenium.BiDi.Input; + +namespace OpenQA.Selenium.BiDi.BrowsingContext; + +public interface IBrowsingContextInputModule +{ + Task OnFileDialogOpenedAsync(Func handler, ContextSubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnFileDialogOpenedAsync(Action handler, ContextSubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task PerformActionsAsync(IEnumerable actions, PerformActionsOptions? options = null, CancellationToken cancellationToken = default); + Task ReleaseActionsAsync(ReleaseActionsOptions? options = null, CancellationToken cancellationToken = default); + Task SetFilesAsync(Script.ISharedReference element, IEnumerable files, SetFilesOptions? options = null, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextLogModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextLogModule.cs new file mode 100644 index 0000000000000..74d5c0f22d90e --- /dev/null +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextLogModule.cs @@ -0,0 +1,28 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using OpenQA.Selenium.BiDi.Log; + +namespace OpenQA.Selenium.BiDi.BrowsingContext; + +public interface IBrowsingContextLogModule +{ + Task OnEntryAddedAsync(Func handler, ContextSubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnEntryAddedAsync(Action handler, ContextSubscriptionOptions? options = null, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs new file mode 100644 index 0000000000000..bd99fb34474b1 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs @@ -0,0 +1,64 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +namespace OpenQA.Selenium.BiDi.BrowsingContext; + +public interface IBrowsingContextModule +{ + Task ActivateAsync(BrowsingContext context, ActivateOptions? options = null, CancellationToken cancellationToken = default); + Task CaptureScreenshotAsync(BrowsingContext context, CaptureScreenshotOptions? options = null, CancellationToken cancellationToken = default); + Task CloseAsync(BrowsingContext context, CloseOptions? options = null, CancellationToken cancellationToken = default); + Task CreateAsync(ContextType type, CreateOptions? options = null, CancellationToken cancellationToken = default); + Task GetTreeAsync(GetTreeOptions? options = null, CancellationToken cancellationToken = default); + Task HandleUserPromptAsync(BrowsingContext context, HandleUserPromptOptions? options = null, CancellationToken cancellationToken = default); + Task LocateNodesAsync(BrowsingContext context, Locator locator, LocateNodesOptions? options = null, CancellationToken cancellationToken = default); + Task NavigateAsync(BrowsingContext context, string url, NavigateOptions? options = null, CancellationToken cancellationToken = default); + Task OnContextCreatedAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnContextCreatedAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnContextDestroyedAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnContextDestroyedAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnDomContentLoadedAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnDomContentLoadedAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnDownloadEndAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnDownloadEndAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnDownloadWillBeginAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnDownloadWillBeginAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnFragmentNavigatedAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnFragmentNavigatedAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnHistoryUpdatedAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnHistoryUpdatedAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnLoadAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnLoadAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnNavigationAbortedAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnNavigationAbortedAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnNavigationCommittedAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnNavigationCommittedAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnNavigationFailedAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnNavigationFailedAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnNavigationStartedAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnNavigationStartedAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnUserPromptClosedAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnUserPromptClosedAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnUserPromptOpenedAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnUserPromptOpenedAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task PrintAsync(BrowsingContext context, PrintOptions? options = null, CancellationToken cancellationToken = default); + Task ReloadAsync(BrowsingContext context, ReloadOptions? options = null, CancellationToken cancellationToken = default); + Task SetViewportAsync(SetViewportOptions? options = null, CancellationToken cancellationToken = default); + Task TraverseHistoryAsync(BrowsingContext context, int delta, TraverseHistoryOptions? options = null, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextNetworkModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextNetworkModule.cs new file mode 100644 index 0000000000000..11be308511e66 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextNetworkModule.cs @@ -0,0 +1,41 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using OpenQA.Selenium.BiDi.Network; + +namespace OpenQA.Selenium.BiDi.BrowsingContext; + +public interface IBrowsingContextNetworkModule +{ + Task AddDataCollectorAsync(IEnumerable dataTypes, int maxEncodedDataSize, ContextAddDataCollectorOptions? options = null, CancellationToken cancellationToken = default); + Task InterceptAuthAsync(Func handler, InterceptAuthOptions? options = null, CancellationToken cancellationToken = default); + Task InterceptRequestAsync(Func handler, InterceptRequestOptions? options = null, CancellationToken cancellationToken = default); + Task InterceptResponseAsync(Func handler, InterceptResponseOptions? options = null, CancellationToken cancellationToken = default); + Task OnAuthRequiredAsync(Func handler, ContextSubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnAuthRequiredAsync(Action handler, ContextSubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnBeforeRequestSentAsync(Func handler, ContextSubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnBeforeRequestSentAsync(Action handler, ContextSubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnFetchErrorAsync(Func handler, ContextSubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnFetchErrorAsync(Action handler, ContextSubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnResponseCompletedAsync(Func handler, ContextSubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnResponseCompletedAsync(Action handler, ContextSubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnResponseStartedAsync(Func handler, ContextSubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnResponseStartedAsync(Action handler, ContextSubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task SetCacheBehaviorAsync(CacheBehavior behavior, ContextSetCacheBehaviorOptions? options = null, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextScriptModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextScriptModule.cs new file mode 100644 index 0000000000000..94885ef7af75c --- /dev/null +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextScriptModule.cs @@ -0,0 +1,33 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using System.Diagnostics.CodeAnalysis; +using OpenQA.Selenium.BiDi.Script; + +namespace OpenQA.Selenium.BiDi.BrowsingContext; + +public interface IBrowsingContextScriptModule +{ + Task AddPreloadScriptAsync([StringSyntax("javascript")] string functionDeclaration, ContextAddPreloadScriptOptions? options = null, CancellationToken cancellationToken = default); + Task CallFunctionAsync([StringSyntax("javascript")] string functionDeclaration, bool awaitPromise, CallFunctionOptions? options = null, ContextTargetOptions? targetOptions = null, CancellationToken cancellationToken = default); + Task CallFunctionAsync([StringSyntax("javascript")] string functionDeclaration, bool awaitPromise, CallFunctionOptions? options = null, ContextTargetOptions? targetOptions = null, CancellationToken cancellationToken = default); + Task EvaluateAsync([StringSyntax("javascript")] string expression, bool awaitPromise, EvaluateOptions? options = null, ContextTargetOptions? targetOptions = null, CancellationToken cancellationToken = default); + Task EvaluateAsync([StringSyntax("javascript")] string expression, bool awaitPromise, EvaluateOptions? options = null, ContextTargetOptions? targetOptions = null, CancellationToken cancellationToken = default); + Task GetRealmsAsync(ContextGetRealmsOptions? options = null, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextStorageModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextStorageModule.cs new file mode 100644 index 0000000000000..19e18ee6b3060 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextStorageModule.cs @@ -0,0 +1,29 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using OpenQA.Selenium.BiDi.Storage; + +namespace OpenQA.Selenium.BiDi.BrowsingContext; + +public interface IBrowsingContextStorageModule +{ + Task DeleteCookiesAsync(ContextDeleteCookiesOptions? options = null, CancellationToken cancellationToken = default); + Task GetCookiesAsync(ContextGetCookiesOptions? options = null, CancellationToken cancellationToken = default); + Task SetCookieAsync(PartialCookie cookie, ContextSetCookieOptions? options = null, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/webdriver/BiDi/Emulation/EmulationModule.cs b/dotnet/src/webdriver/BiDi/Emulation/EmulationModule.cs index 476e9178de5a3..a6d6dab6107df 100644 --- a/dotnet/src/webdriver/BiDi/Emulation/EmulationModule.cs +++ b/dotnet/src/webdriver/BiDi/Emulation/EmulationModule.cs @@ -23,7 +23,7 @@ namespace OpenQA.Selenium.BiDi.Emulation; -public sealed class EmulationModule : Module +public sealed class EmulationModule : Module, IEmulationModule { private EmulationJsonSerializerContext _jsonContext = null!; @@ -113,7 +113,7 @@ public async Task SetNetworkConditionsAsync(NetworkC return await ExecuteCommandAsync(new SetNetworkConditionsCommand(@params), options, _jsonContext.SetNetworkConditionsCommand, _jsonContext.SetNetworkConditionsResult, cancellationToken).ConfigureAwait(false); } - protected override void Initialize(BiDi bidi, JsonSerializerOptions jsonSerializerOptions) + protected override void Initialize(IBiDi bidi, JsonSerializerOptions jsonSerializerOptions) { jsonSerializerOptions.Converters.Add(new BrowsingContextConverter(bidi)); jsonSerializerOptions.Converters.Add(new BrowserUserContextConverter(bidi)); diff --git a/dotnet/src/webdriver/BiDi/Emulation/IEmulationModule.cs b/dotnet/src/webdriver/BiDi/Emulation/IEmulationModule.cs new file mode 100644 index 0000000000000..3b157b0876356 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Emulation/IEmulationModule.cs @@ -0,0 +1,36 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +namespace OpenQA.Selenium.BiDi.Emulation; + +public interface IEmulationModule +{ + Task SetForcedColorsModeThemeOverrideAsync(ForcedColorsModeTheme? theme, SetForcedColorsModeThemeOverrideOptions? options = null, CancellationToken cancellationToken = default); + Task SetGeolocationCoordinatesOverrideAsync(double latitude, double longitude, SetGeolocationCoordinatesOverrideOptions? options = null, CancellationToken cancellationToken = default); + Task SetGeolocationCoordinatesOverrideAsync(SetGeolocationOverrideOptions? options = null, CancellationToken cancellationToken = default); + Task SetGeolocationPositionErrorOverrideAsync(SetGeolocationPositionErrorOverrideOptions? options = null, CancellationToken cancellationToken = default); + Task SetLocaleOverrideAsync(string? locale, SetLocaleOverrideOptions? options = null, CancellationToken cancellationToken = default); + Task SetNetworkConditionsAsync(NetworkConditions? networkConditions, SetNetworkConditionsOptions? options = null, CancellationToken cancellationToken = default); + Task SetScreenOrientationOverrideAsync(ScreenOrientation? screenOrientation, SetScreenOrientationOverrideOptions? options = null, CancellationToken cancellationToken = default); + Task SetScreenSettingsOverrideAsync(ScreenArea? screenArea, SetScreenSettingsOverrideOptions? options = null, CancellationToken cancellationToken = default); + Task SetScriptingEnabledAsync(bool? enabled, SetScriptingEnabledOptions? options = null, CancellationToken cancellationToken = default); + Task SetTimezoneOverrideAsync(string? timezone, SetTimezoneOverrideOptions? options = null, CancellationToken cancellationToken = default); + Task SetTouchOverrideAsync(long? maxTouchPoints, SetTouchOverrideOptions? options = null, CancellationToken cancellationToken = default); + Task SetUserAgentOverrideAsync(string? userAgent, SetUserAgentOverrideOptions? options = null, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/webdriver/BiDi/EventArgs.cs b/dotnet/src/webdriver/BiDi/EventArgs.cs index bd6ecb9c7d339..691b62ad32250 100644 --- a/dotnet/src/webdriver/BiDi/EventArgs.cs +++ b/dotnet/src/webdriver/BiDi/EventArgs.cs @@ -23,10 +23,10 @@ namespace OpenQA.Selenium.BiDi; public abstract record EventArgs { - private BiDi? _bidi; + private IBiDi? _bidi; [JsonIgnore] - public BiDi BiDi + public IBiDi BiDi { get => _bidi ?? throw new InvalidOperationException($"{nameof(BiDi)} instance has not been hydrated."); internal set => _bidi = value; diff --git a/dotnet/src/webdriver/BiDi/EventDispatcher.cs b/dotnet/src/webdriver/BiDi/EventDispatcher.cs new file mode 100644 index 0000000000000..1a4e93006821c --- /dev/null +++ b/dotnet/src/webdriver/BiDi/EventDispatcher.cs @@ -0,0 +1,137 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using System.Collections.Concurrent; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using System.Threading.Channels; +using OpenQA.Selenium.BiDi.Session; +using OpenQA.Selenium.Internal.Logging; + +namespace OpenQA.Selenium.BiDi; + +internal sealed class EventDispatcher : IAsyncDisposable +{ + private readonly ILogger _logger = Internal.Logging.Log.GetLogger(); + + private readonly Func _sessionProvider; + + private readonly ConcurrentDictionary _events = new(); + + private readonly Channel _pendingEvents = Channel.CreateUnbounded(new() + { + SingleReader = true, + SingleWriter = true + }); + + private readonly Task _eventEmitterTask; + + private static readonly TaskFactory _myTaskFactory = new(CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskContinuationOptions.None, TaskScheduler.Default); + + public EventDispatcher(Func sessionProvider) + { + _sessionProvider = sessionProvider; + _eventEmitterTask = _myTaskFactory.StartNew(ProcessEventsAwaiterAsync).Unwrap(); + } + + public async Task SubscribeAsync(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken) + where TEventArgs : EventArgs + { + var registration = _events.GetOrAdd(eventName, _ => new EventRegistration(jsonTypeInfo)); + + var subscribeResult = await _sessionProvider().SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken).ConfigureAwait(false); + + registration.Handlers.Add(eventHandler); + + return new Subscription(subscribeResult.Subscription, this, eventHandler); + } + + public async ValueTask UnsubscribeAsync(Subscription subscription, CancellationToken cancellationToken) + { + if (_events.TryGetValue(subscription.EventHandler.EventName, out var registration)) + { + await _sessionProvider().UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false); + registration.Handlers.Remove(subscription.EventHandler); + } + } + + public void EnqueueEvent(string method, ReadOnlyMemory jsonUtf8Bytes, IBiDi bidi) + { + if (_events.TryGetValue(method, out var registration) && registration.TypeInfo is not null) + { + _pendingEvents.Writer.TryWrite(new PendingEvent(method, jsonUtf8Bytes, bidi, registration.TypeInfo)); + } + else + { + if (_logger.IsEnabled(LogEventLevel.Warn)) + { + _logger.Warn($"Received BiDi event with method '{method}', but no event type mapping was found. Event will be ignored."); + } + } + } + + private async Task ProcessEventsAwaiterAsync() + { + var reader = _pendingEvents.Reader; + while (await reader.WaitToReadAsync().ConfigureAwait(false)) + { + while (reader.TryRead(out var result)) + { + try + { + if (_events.TryGetValue(result.Method, out var registration)) + { + // Deserialize on background thread instead of network thread (single parse) + var eventArgs = (EventArgs)JsonSerializer.Deserialize(result.JsonUtf8Bytes.Span, result.TypeInfo)!; + eventArgs.BiDi = result.BiDi; + + foreach (var handler in registration.Handlers.ToArray()) // copy handlers avoiding modified collection while iterating + { + await handler.InvokeAsync(eventArgs).ConfigureAwait(false); + } + } + } + catch (Exception ex) + { + if (_logger.IsEnabled(LogEventLevel.Error)) + { + _logger.Error($"Unhandled error processing BiDi event handler: {ex}"); + } + } + } + } + } + + public async ValueTask DisposeAsync() + { + _pendingEvents.Writer.Complete(); + + await _eventEmitterTask.ConfigureAwait(false); + + GC.SuppressFinalize(this); + } + + private readonly record struct PendingEvent(string Method, ReadOnlyMemory JsonUtf8Bytes, IBiDi BiDi, JsonTypeInfo TypeInfo); + + private sealed class EventRegistration(JsonTypeInfo typeInfo) + { + public JsonTypeInfo TypeInfo { get; } = typeInfo; + public List Handlers { get; } = []; + } +} diff --git a/dotnet/src/webdriver/BiDi/IBiDi.cs b/dotnet/src/webdriver/BiDi/IBiDi.cs new file mode 100644 index 0000000000000..c7f5d13765e78 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/IBiDi.cs @@ -0,0 +1,62 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using System.ComponentModel; +using OpenQA.Selenium.BiDi.Browser; +using OpenQA.Selenium.BiDi.BrowsingContext; +using OpenQA.Selenium.BiDi.Emulation; +using OpenQA.Selenium.BiDi.Input; +using OpenQA.Selenium.BiDi.Log; +using OpenQA.Selenium.BiDi.Network; +using OpenQA.Selenium.BiDi.Script; +using OpenQA.Selenium.BiDi.Session; +using OpenQA.Selenium.BiDi.Storage; +using OpenQA.Selenium.BiDi.WebExtension; + +namespace OpenQA.Selenium.BiDi; + +public interface IBiDi : IAsyncDisposable +{ + IBrowserModule Browser { get; } + + IBrowsingContextModule BrowsingContext { get; } + + IEmulationModule Emulation { get; } + + IInputModule Input { get; } + + ILogModule Log { get; } + + INetworkModule Network { get; } + + IScriptModule Script { get; } + + IStorageModule Storage { get; } + + IWebExtensionModule WebExtension { get; } + + Task StatusAsync(StatusOptions? options = null, CancellationToken cancellationToken = default); + + Task NewAsync(CapabilitiesRequest capabilities, NewOptions? options = null, CancellationToken cancellationToken = default); + + Task EndAsync(EndOptions? options = null, CancellationToken cancellationToken = default); + + [EditorBrowsable(EditorBrowsableState.Never)] + T AsModule() where T : Module, new(); +} diff --git a/dotnet/src/webdriver/BiDi/ITransport.cs b/dotnet/src/webdriver/BiDi/ITransport.cs index 65e38493ca737..bdf33406b3936 100644 --- a/dotnet/src/webdriver/BiDi/ITransport.cs +++ b/dotnet/src/webdriver/BiDi/ITransport.cs @@ -21,8 +21,6 @@ namespace OpenQA.Selenium.BiDi; interface ITransport : IDisposable { - Task ConnectAsync(CancellationToken cancellationToken); - Task ReceiveAsync(CancellationToken cancellationToken); Task SendAsync(byte[] data, CancellationToken cancellationToken); diff --git a/dotnet/src/webdriver/BiDi/Input/IInputModule.cs b/dotnet/src/webdriver/BiDi/Input/IInputModule.cs new file mode 100644 index 0000000000000..5730b806133ee --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Input/IInputModule.cs @@ -0,0 +1,29 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +namespace OpenQA.Selenium.BiDi.Input; + +public interface IInputModule +{ + Task OnFileDialogOpenedAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnFileDialogOpenedAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task PerformActionsAsync(BrowsingContext.BrowsingContext context, IEnumerable actions, PerformActionsOptions? options = null, CancellationToken cancellationToken = default); + Task ReleaseActionsAsync(BrowsingContext.BrowsingContext context, ReleaseActionsOptions? options = null, CancellationToken cancellationToken = default); + Task SetFilesAsync(BrowsingContext.BrowsingContext context, Script.ISharedReference element, IEnumerable files, SetFilesOptions? options = null, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/webdriver/BiDi/Input/InputModule.cs b/dotnet/src/webdriver/BiDi/Input/InputModule.cs index e31287532c6ec..f6c72fd81952a 100644 --- a/dotnet/src/webdriver/BiDi/Input/InputModule.cs +++ b/dotnet/src/webdriver/BiDi/Input/InputModule.cs @@ -23,7 +23,7 @@ namespace OpenQA.Selenium.BiDi.Input; -public sealed class InputModule : Module +public sealed class InputModule : Module, IInputModule { private InputJsonSerializerContext _jsonContext = null!; @@ -58,7 +58,7 @@ public async Task OnFileDialogOpenedAsync(Action +internal class BrowserUserContextConverter(IBiDi bidi) : JsonConverter { public override UserContext? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/dotnet/src/webdriver/BiDi/Json/Converters/BrowsingContextConverter.cs b/dotnet/src/webdriver/BiDi/Json/Converters/BrowsingContextConverter.cs index 2a5a9f7edb14d..e91923d49fa01 100644 --- a/dotnet/src/webdriver/BiDi/Json/Converters/BrowsingContextConverter.cs +++ b/dotnet/src/webdriver/BiDi/Json/Converters/BrowsingContextConverter.cs @@ -22,14 +22,9 @@ namespace OpenQA.Selenium.BiDi.Json.Converters; -internal class BrowsingContextConverter : JsonConverter +internal class BrowsingContextConverter(IBiDi bidi) : JsonConverter { - private readonly BiDi _bidi; - - public BrowsingContextConverter(BiDi bidi) - { - _bidi = bidi; - } + private readonly IBiDi _bidi = bidi; public override BrowsingContext.BrowsingContext? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/dotnet/src/webdriver/BiDi/Json/Converters/CollectorConverter.cs b/dotnet/src/webdriver/BiDi/Json/Converters/CollectorConverter.cs index 74ba4c6c8cca8..bce894575a2e7 100644 --- a/dotnet/src/webdriver/BiDi/Json/Converters/CollectorConverter.cs +++ b/dotnet/src/webdriver/BiDi/Json/Converters/CollectorConverter.cs @@ -23,7 +23,7 @@ namespace OpenQA.Selenium.BiDi.Json.Converters; -internal class CollectorConverter(BiDi bidi) : JsonConverter +internal class CollectorConverter(IBiDi bidi) : JsonConverter { public override Collector? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/dotnet/src/webdriver/BiDi/Json/Converters/HandleConverter.cs b/dotnet/src/webdriver/BiDi/Json/Converters/HandleConverter.cs index 788fc216406a5..08d455c26476b 100644 --- a/dotnet/src/webdriver/BiDi/Json/Converters/HandleConverter.cs +++ b/dotnet/src/webdriver/BiDi/Json/Converters/HandleConverter.cs @@ -23,7 +23,7 @@ namespace OpenQA.Selenium.BiDi.Json.Converters; -internal class HandleConverter(BiDi bidi) : JsonConverter +internal class HandleConverter(IBiDi bidi) : JsonConverter { public override Handle? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/dotnet/src/webdriver/BiDi/Json/Converters/InterceptConverter.cs b/dotnet/src/webdriver/BiDi/Json/Converters/InterceptConverter.cs index 2d29b0e8abfb0..e2ba43eba25aa 100644 --- a/dotnet/src/webdriver/BiDi/Json/Converters/InterceptConverter.cs +++ b/dotnet/src/webdriver/BiDi/Json/Converters/InterceptConverter.cs @@ -23,7 +23,7 @@ namespace OpenQA.Selenium.BiDi.Json.Converters; -internal class InterceptConverter(BiDi bidi) : JsonConverter +internal class InterceptConverter(IBiDi bidi) : JsonConverter { public override Intercept? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/dotnet/src/webdriver/BiDi/Json/Converters/InternalIdConverter.cs b/dotnet/src/webdriver/BiDi/Json/Converters/InternalIdConverter.cs index 5de5e79f1c28f..0d8c1b8a8b3cb 100644 --- a/dotnet/src/webdriver/BiDi/Json/Converters/InternalIdConverter.cs +++ b/dotnet/src/webdriver/BiDi/Json/Converters/InternalIdConverter.cs @@ -23,7 +23,7 @@ namespace OpenQA.Selenium.BiDi.Json.Converters; -internal class InternalIdConverter(BiDi bidi) : JsonConverter +internal class InternalIdConverter(IBiDi bidi) : JsonConverter { public override InternalId? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/dotnet/src/webdriver/BiDi/Json/Converters/PreloadScriptConverter.cs b/dotnet/src/webdriver/BiDi/Json/Converters/PreloadScriptConverter.cs index 858bd3268d42e..868d31b6d2623 100644 --- a/dotnet/src/webdriver/BiDi/Json/Converters/PreloadScriptConverter.cs +++ b/dotnet/src/webdriver/BiDi/Json/Converters/PreloadScriptConverter.cs @@ -23,7 +23,7 @@ namespace OpenQA.Selenium.BiDi.Json.Converters; -internal class PreloadScriptConverter(BiDi bidi) : JsonConverter +internal class PreloadScriptConverter(IBiDi bidi) : JsonConverter { public override PreloadScript? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/dotnet/src/webdriver/BiDi/Json/Converters/RealmConverter.cs b/dotnet/src/webdriver/BiDi/Json/Converters/RealmConverter.cs index 0d408a3c06e01..5afe9866f949d 100644 --- a/dotnet/src/webdriver/BiDi/Json/Converters/RealmConverter.cs +++ b/dotnet/src/webdriver/BiDi/Json/Converters/RealmConverter.cs @@ -23,7 +23,7 @@ namespace OpenQA.Selenium.BiDi.Json.Converters; -internal class RealmConverter(BiDi bidi) : JsonConverter +internal class RealmConverter(IBiDi bidi) : JsonConverter { public override Realm? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/dotnet/src/webdriver/BiDi/Json/Converters/WebExtensionConverter.cs b/dotnet/src/webdriver/BiDi/Json/Converters/WebExtensionConverter.cs index 42929f278adb4..9f8b74e52d6fa 100644 --- a/dotnet/src/webdriver/BiDi/Json/Converters/WebExtensionConverter.cs +++ b/dotnet/src/webdriver/BiDi/Json/Converters/WebExtensionConverter.cs @@ -23,7 +23,7 @@ namespace OpenQA.Selenium.BiDi.Json.Converters; -internal class WebExtensionConverter(BiDi bidi) : JsonConverter +internal class WebExtensionConverter(IBiDi bidi) : JsonConverter { public override Extension? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/dotnet/src/webdriver/BiDi/Log/ILogModule.cs b/dotnet/src/webdriver/BiDi/Log/ILogModule.cs new file mode 100644 index 0000000000000..8accd921ccf56 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Log/ILogModule.cs @@ -0,0 +1,26 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +namespace OpenQA.Selenium.BiDi.Log; + +public interface ILogModule +{ + Task OnEntryAddedAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnEntryAddedAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/webdriver/BiDi/Log/LogModule.cs b/dotnet/src/webdriver/BiDi/Log/LogModule.cs index 843f92e0a293b..66cb735dce597 100644 --- a/dotnet/src/webdriver/BiDi/Log/LogModule.cs +++ b/dotnet/src/webdriver/BiDi/Log/LogModule.cs @@ -23,7 +23,7 @@ namespace OpenQA.Selenium.BiDi.Log; -public sealed class LogModule : Module +public sealed class LogModule : Module, ILogModule { private LogJsonSerializerContext _jsonContext = null!; @@ -37,7 +37,7 @@ public async Task OnEntryAddedAsync(Action hand return await SubscribeAsync("log.entryAdded", handler, options, _jsonContext.LogEntryEventArgs, cancellationToken).ConfigureAwait(false); } - protected override void Initialize(BiDi bidi, JsonSerializerOptions jsonSerializerOptions) + protected override void Initialize(IBiDi bidi, JsonSerializerOptions jsonSerializerOptions) { jsonSerializerOptions.Converters.Add(new BrowsingContextConverter(bidi)); jsonSerializerOptions.Converters.Add(new BrowserUserContextConverter(bidi)); diff --git a/dotnet/src/webdriver/BiDi/Module.cs b/dotnet/src/webdriver/BiDi/Module.cs index 41407aa0913a0..715a5499bfc10 100644 --- a/dotnet/src/webdriver/BiDi/Module.cs +++ b/dotnet/src/webdriver/BiDi/Module.cs @@ -47,9 +47,9 @@ public Task SubscribeAsync(string eventName, Func(BiDi bidi, Broker broker, JsonSerializerOptions jsonSerializerOptions) + internal static TModule Create(IBiDi bidi, Broker broker, JsonSerializerOptions jsonSerializerOptions) where TModule : Module, new() { TModule module = new() diff --git a/dotnet/src/webdriver/BiDi/Network/Collector.cs b/dotnet/src/webdriver/BiDi/Network/Collector.cs index 0782d128e7898..5cb1bfe23cbe3 100644 --- a/dotnet/src/webdriver/BiDi/Network/Collector.cs +++ b/dotnet/src/webdriver/BiDi/Network/Collector.cs @@ -24,7 +24,7 @@ namespace OpenQA.Selenium.BiDi.Network; public sealed record Collector { - public Collector(BiDi bidi, string id) + public Collector(IBiDi bidi, string id) : this(id) { BiDi = bidi ?? throw new ArgumentNullException(nameof(bidi)); @@ -37,10 +37,10 @@ internal Collector(string id) } internal string Id { get; } - private BiDi? _bidi; + private IBiDi? _bidi; [JsonIgnore] - public BiDi BiDi + public IBiDi BiDi { get => _bidi ?? throw new InvalidOperationException($"{nameof(BiDi)} instance has not been hydrated."); internal set => _bidi = value; diff --git a/dotnet/src/webdriver/BiDi/Network/INetworkModule.cs b/dotnet/src/webdriver/BiDi/Network/INetworkModule.cs new file mode 100644 index 0000000000000..cddfadeed3101 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Network/INetworkModule.cs @@ -0,0 +1,51 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +namespace OpenQA.Selenium.BiDi.Network; + +public interface INetworkModule +{ + Task AddDataCollectorAsync(IEnumerable dataTypes, int maxEncodedDataSize, AddDataCollectorOptions? options = null, CancellationToken cancellationToken = default); + Task AddInterceptAsync(IEnumerable phases, AddInterceptOptions? options = null, CancellationToken cancellationToken = default); + Task ContinueRequestAsync(Request request, ContinueRequestOptions? options = null, CancellationToken cancellationToken = default); + Task ContinueResponseAsync(Request request, ContinueResponseOptions? options = null, CancellationToken cancellationToken = default); + Task ContinueWithAuthAsync(Request request, AuthCredentials credentials, ContinueWithAuthCredentialsOptions? options = null, CancellationToken cancellationToken = default); + Task ContinueWithAuthAsync(Request request, ContinueWithAuthDefaultCredentialsOptions? options = null, CancellationToken cancellationToken = default); + Task ContinueWithAuthAsync(Request request, ContinueWithAuthCancelCredentialsOptions? options = null, CancellationToken cancellationToken = default); + Task FailRequestAsync(Request request, FailRequestOptions? options = null, CancellationToken cancellationToken = default); + Task GetDataAsync(DataType dataType, Request request, GetDataOptions? options = null, CancellationToken cancellationToken = default); + Task InterceptAuthAsync(Func handler, InterceptAuthOptions? options = null, CancellationToken cancellationToken = default); + Task InterceptRequestAsync(Func handler, InterceptRequestOptions? options = null, CancellationToken cancellationToken = default); + Task InterceptResponseAsync(Func handler, InterceptResponseOptions? options = null, CancellationToken cancellationToken = default); + Task OnAuthRequiredAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnAuthRequiredAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnBeforeRequestSentAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnBeforeRequestSentAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnFetchErrorAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnFetchErrorAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnResponseCompletedAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnResponseCompletedAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnResponseStartedAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnResponseStartedAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task ProvideResponseAsync(Request request, ProvideResponseOptions? options = null, CancellationToken cancellationToken = default); + Task RemoveDataCollectorAsync(Collector collector, RemoveDataCollectorOptions? options = null, CancellationToken cancellationToken = default); + Task RemoveInterceptAsync(Intercept intercept, RemoveInterceptOptions? options = null, CancellationToken cancellationToken = default); + Task SetCacheBehaviorAsync(CacheBehavior behavior, SetCacheBehaviorOptions? options = null, CancellationToken cancellationToken = default); + Task SetExtraHeadersAsync(IEnumerable
headers, SetExtraHeadersOptions? options = null, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/webdriver/BiDi/Network/Intercept.cs b/dotnet/src/webdriver/BiDi/Network/Intercept.cs index e8628734220df..c8b8e434ae85b 100644 --- a/dotnet/src/webdriver/BiDi/Network/Intercept.cs +++ b/dotnet/src/webdriver/BiDi/Network/Intercept.cs @@ -24,7 +24,7 @@ namespace OpenQA.Selenium.BiDi.Network; public sealed record Intercept { - public Intercept(BiDi bidi, string id) + public Intercept(IBiDi bidi, string id) : this(id) { BiDi = bidi ?? throw new ArgumentNullException(nameof(bidi)); @@ -38,10 +38,10 @@ internal Intercept(string id) internal string Id { get; } - private BiDi? _bidi; + private IBiDi? _bidi; [JsonIgnore] - public BiDi BiDi + public IBiDi BiDi { get => _bidi ?? throw new InvalidOperationException($"{nameof(BiDi)} instance has not been hydrated."); internal set => _bidi = value; diff --git a/dotnet/src/webdriver/BiDi/Network/NetworkModule.HighLevel.cs b/dotnet/src/webdriver/BiDi/Network/NetworkModule.HighLevel.cs index 4980c7e348a82..3c20db64dc84e 100644 --- a/dotnet/src/webdriver/BiDi/Network/NetworkModule.HighLevel.cs +++ b/dotnet/src/webdriver/BiDi/Network/NetworkModule.HighLevel.cs @@ -63,7 +63,7 @@ public sealed record InterceptAuthOptions : AddInterceptOptions; public sealed record InterceptedRequest : BeforeRequestSentEventArgs { - internal InterceptedRequest(BiDi bidi, BrowsingContext.BrowsingContext? context, bool isBlocked, BrowsingContext.Navigation? navigation, long redirectCount, RequestData request, DateTimeOffset timestamp, Initiator initiator, IReadOnlyList? intercepts) + internal InterceptedRequest(IBiDi bidi, BrowsingContext.BrowsingContext? context, bool isBlocked, BrowsingContext.Navigation? navigation, long redirectCount, RequestData request, DateTimeOffset timestamp, Initiator initiator, IReadOnlyList? intercepts) : base(context, isBlocked, navigation, redirectCount, request, timestamp, initiator, intercepts) { BiDi = bidi; @@ -87,7 +87,7 @@ public Task ProvideResponseAsync(ProvideResponseOptions? options = null, Cancell public sealed record InterceptedResponse : ResponseStartedEventArgs { - internal InterceptedResponse(BiDi bidi, BrowsingContext.BrowsingContext? context, bool isBlocked, BrowsingContext.Navigation? navigation, long redirectCount, RequestData request, DateTimeOffset timestamp, ResponseData response, IReadOnlyList? intercepts) + internal InterceptedResponse(IBiDi bidi, BrowsingContext.BrowsingContext? context, bool isBlocked, BrowsingContext.Navigation? navigation, long redirectCount, RequestData request, DateTimeOffset timestamp, ResponseData response, IReadOnlyList? intercepts) : base(context, isBlocked, navigation, redirectCount, request, timestamp, response, intercepts) { BiDi = bidi; @@ -101,7 +101,7 @@ public Task ContinueAsync(ContinueResponseOptions? options = null, CancellationT public sealed record InterceptedAuth : AuthRequiredEventArgs { - internal InterceptedAuth(BiDi bidi, BrowsingContext.BrowsingContext? context, bool IsBlocked, BrowsingContext.Navigation? navigation, long redirectCount, RequestData request, DateTimeOffset timestamp, ResponseData response, IReadOnlyList? intercepts) + internal InterceptedAuth(IBiDi bidi, BrowsingContext.BrowsingContext? context, bool IsBlocked, BrowsingContext.Navigation? navigation, long redirectCount, RequestData request, DateTimeOffset timestamp, ResponseData response, IReadOnlyList? intercepts) : base(context, IsBlocked, navigation, redirectCount, request, timestamp, response, intercepts) { BiDi = bidi; @@ -123,7 +123,7 @@ public Task ContinueAsync(ContinueWithAuthCancelCredentialsOptions? options = nu } } -public sealed record Interception(NetworkModule Network, Intercept Intercept) : IAsyncDisposable +public sealed record Interception(INetworkModule Network, Intercept Intercept) : IAsyncDisposable { IList OnBeforeRequestSentSubscriptions { get; } = []; IList OnResponseStartedSubscriptions { get; } = []; diff --git a/dotnet/src/webdriver/BiDi/Network/NetworkModule.cs b/dotnet/src/webdriver/BiDi/Network/NetworkModule.cs index 8654d6eaddbd3..d9f7745dbb3b8 100644 --- a/dotnet/src/webdriver/BiDi/Network/NetworkModule.cs +++ b/dotnet/src/webdriver/BiDi/Network/NetworkModule.cs @@ -23,7 +23,7 @@ namespace OpenQA.Selenium.BiDi.Network; -public sealed partial class NetworkModule : Module +public sealed partial class NetworkModule : Module, INetworkModule { private NetworkJsonSerializerContext _jsonContext = null!; @@ -171,7 +171,7 @@ public async Task OnAuthRequiredAsync(Action +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +namespace OpenQA.Selenium.BiDi.Permissions; + +public interface IPermissionsModule +{ + Task SetPermissionAsync(PermissionDescriptor descriptor, PermissionState state, string origin, SetPermissionOptions? options = null, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/webdriver/BiDi/Permissions/PermissionsBiDiExtensions.cs b/dotnet/src/webdriver/BiDi/Permissions/PermissionsBiDiExtensions.cs index 24fa50a19d4fd..c1b6e354a26af 100644 --- a/dotnet/src/webdriver/BiDi/Permissions/PermissionsBiDiExtensions.cs +++ b/dotnet/src/webdriver/BiDi/Permissions/PermissionsBiDiExtensions.cs @@ -21,7 +21,7 @@ namespace OpenQA.Selenium.BiDi.Permissions; public static class PermissionsBiDiExtensions { - public static PermissionsModule AsPermissions(this BiDi bidi) + public static IPermissionsModule AsPermissions(this IBiDi bidi) { if (bidi is null) { diff --git a/dotnet/src/webdriver/BiDi/Permissions/PermissionsModule.cs b/dotnet/src/webdriver/BiDi/Permissions/PermissionsModule.cs index c2c3c49186edc..04ab29651496a 100644 --- a/dotnet/src/webdriver/BiDi/Permissions/PermissionsModule.cs +++ b/dotnet/src/webdriver/BiDi/Permissions/PermissionsModule.cs @@ -23,7 +23,7 @@ namespace OpenQA.Selenium.BiDi.Permissions; -public sealed class PermissionsModule : Module +public sealed class PermissionsModule : Module, IPermissionsModule { private PermissionsJsonSerializerContext _jsonContext = null!; @@ -34,7 +34,7 @@ public async Task SetPermissionAsync(PermissionDescriptor d return await ExecuteCommandAsync(new SetPermissionCommand(@params), options, _jsonContext.SetPermissionCommand, _jsonContext.SetPermissionResult, cancellationToken).ConfigureAwait(false); } - protected override void Initialize(BiDi bidi, JsonSerializerOptions jsonSerializerOptions) + protected override void Initialize(IBiDi bidi, JsonSerializerOptions jsonSerializerOptions) { jsonSerializerOptions.Converters.Add(new BrowserUserContextConverter(bidi)); diff --git a/dotnet/src/webdriver/BiDi/Script/Handle.cs b/dotnet/src/webdriver/BiDi/Script/Handle.cs index 41fab9416a507..3a9e1f75ed5b4 100644 --- a/dotnet/src/webdriver/BiDi/Script/Handle.cs +++ b/dotnet/src/webdriver/BiDi/Script/Handle.cs @@ -24,7 +24,7 @@ namespace OpenQA.Selenium.BiDi.Script; public sealed record Handle { - public Handle(BiDi bidi, string id) + public Handle(IBiDi bidi, string id) : this(id) { BiDi = bidi ?? throw new ArgumentNullException(nameof(bidi)); @@ -38,10 +38,10 @@ internal Handle(string id) internal string Id { get; } - private BiDi? _bidi; + private IBiDi? _bidi; [JsonIgnore] - public BiDi BiDi + public IBiDi BiDi { get => _bidi ?? throw new InvalidOperationException($"{nameof(BiDi)} instance has not been hydrated."); internal set => _bidi = value; diff --git a/dotnet/src/webdriver/BiDi/Script/IScriptModule.cs b/dotnet/src/webdriver/BiDi/Script/IScriptModule.cs new file mode 100644 index 0000000000000..ff45ac79003db --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Script/IScriptModule.cs @@ -0,0 +1,40 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using System.Diagnostics.CodeAnalysis; + +namespace OpenQA.Selenium.BiDi.Script; + +public interface IScriptModule +{ + Task AddPreloadScriptAsync([StringSyntax("javascript")] string functionDeclaration, AddPreloadScriptOptions? options = null, CancellationToken cancellationToken = default); + Task CallFunctionAsync([StringSyntax("javascript")] string functionDeclaration, bool awaitPromise, Target target, CallFunctionOptions? options = null, CancellationToken cancellationToken = default); + Task CallFunctionAsync([StringSyntax("javascript")] string functionDeclaration, bool awaitPromise, Target target, CallFunctionOptions? options = null, CancellationToken cancellationToken = default); + Task DisownAsync(IEnumerable handles, Target target, DisownOptions? options = null, CancellationToken cancellationToken = default); + Task EvaluateAsync([StringSyntax("javascript")] string expression, bool awaitPromise, Target target, EvaluateOptions? options = null, CancellationToken cancellationToken = default); + Task EvaluateAsync([StringSyntax("javascript")] string expression, bool awaitPromise, Target target, EvaluateOptions? options = null, CancellationToken cancellationToken = default); + Task GetRealmsAsync(GetRealmsOptions? options = null, CancellationToken cancellationToken = default); + Task OnMessageAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnMessageAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnRealmCreatedAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnRealmCreatedAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnRealmDestroyedAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnRealmDestroyedAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task RemovePreloadScriptAsync(PreloadScript script, RemovePreloadScriptOptions? options = null, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/webdriver/BiDi/Script/InternalId.cs b/dotnet/src/webdriver/BiDi/Script/InternalId.cs index 4be0e7e53ec00..208af87c6afd5 100644 --- a/dotnet/src/webdriver/BiDi/Script/InternalId.cs +++ b/dotnet/src/webdriver/BiDi/Script/InternalId.cs @@ -24,7 +24,7 @@ namespace OpenQA.Selenium.BiDi.Script; public sealed record InternalId { - public InternalId(BiDi bidi, string id) + public InternalId(IBiDi bidi, string id) : this(id) { BiDi = bidi ?? throw new ArgumentNullException(nameof(bidi)); @@ -38,10 +38,10 @@ internal InternalId(string id) internal string Id { get; } - private BiDi? _bidi; + private IBiDi? _bidi; [JsonIgnore] - public BiDi BiDi + public IBiDi BiDi { get => _bidi ?? throw new InvalidOperationException($"{nameof(BiDi)} instance has not been hydrated."); internal set => _bidi = value; diff --git a/dotnet/src/webdriver/BiDi/Script/PreloadScript.cs b/dotnet/src/webdriver/BiDi/Script/PreloadScript.cs index 215970e5fedfc..37ef96864abdb 100644 --- a/dotnet/src/webdriver/BiDi/Script/PreloadScript.cs +++ b/dotnet/src/webdriver/BiDi/Script/PreloadScript.cs @@ -24,7 +24,7 @@ namespace OpenQA.Selenium.BiDi.Script; public sealed record PreloadScript { - public PreloadScript(BiDi bidi, string id) + public PreloadScript(IBiDi bidi, string id) : this(id) { BiDi = bidi ?? throw new ArgumentNullException(nameof(bidi)); @@ -38,10 +38,10 @@ internal PreloadScript(string id) internal string Id { get; } - private BiDi? _bidi; + private IBiDi? _bidi; [JsonIgnore] - public BiDi BiDi + public IBiDi BiDi { get => _bidi ?? throw new InvalidOperationException($"{nameof(BiDi)} instance has not been hydrated."); internal set => _bidi = value; diff --git a/dotnet/src/webdriver/BiDi/Script/Realm.cs b/dotnet/src/webdriver/BiDi/Script/Realm.cs index e648ad873bbe0..5a8b65948ee70 100644 --- a/dotnet/src/webdriver/BiDi/Script/Realm.cs +++ b/dotnet/src/webdriver/BiDi/Script/Realm.cs @@ -24,7 +24,7 @@ namespace OpenQA.Selenium.BiDi.Script; public sealed record Realm { - public Realm(BiDi bidi, string id) + public Realm(IBiDi bidi, string id) : this(id) { BiDi = bidi ?? throw new ArgumentNullException(nameof(bidi)); @@ -38,10 +38,10 @@ internal Realm(string id) internal string Id { get; } - private BiDi? _bidi; + private IBiDi? _bidi; [JsonIgnore] - public BiDi BiDi + public IBiDi BiDi { get => _bidi ?? throw new InvalidOperationException($"{nameof(BiDi)} instance has not been hydrated."); internal set => _bidi = value; diff --git a/dotnet/src/webdriver/BiDi/Script/ScriptModule.cs b/dotnet/src/webdriver/BiDi/Script/ScriptModule.cs index 59b98dfe6904d..eaf2f6f26dcdf 100644 --- a/dotnet/src/webdriver/BiDi/Script/ScriptModule.cs +++ b/dotnet/src/webdriver/BiDi/Script/ScriptModule.cs @@ -24,7 +24,7 @@ namespace OpenQA.Selenium.BiDi.Script; -public sealed class ScriptModule : Module +public sealed class ScriptModule : Module, IScriptModule { private ScriptJsonSerializerContext _jsonContext = null!; @@ -114,7 +114,7 @@ public async Task OnRealmDestroyedAsync(Action +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +namespace OpenQA.Selenium.BiDi.Session; + +internal interface ISessionModule +{ + Task EndAsync(EndOptions? options = null, CancellationToken cancellationToken = default); + Task NewAsync(CapabilitiesRequest capabilities, NewOptions? options = null, CancellationToken cancellationToken = default); + Task StatusAsync(StatusOptions? options = null, CancellationToken cancellationToken = default); + Task SubscribeAsync(IEnumerable events, SubscribeOptions? options = null, CancellationToken cancellationToken = default); + Task UnsubscribeAsync(IEnumerable subscriptions, UnsubscribeByIdOptions? options = null, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/webdriver/BiDi/Session/SessionModule.cs b/dotnet/src/webdriver/BiDi/Session/SessionModule.cs index 6c20c2986f5e2..690ec490893ad 100644 --- a/dotnet/src/webdriver/BiDi/Session/SessionModule.cs +++ b/dotnet/src/webdriver/BiDi/Session/SessionModule.cs @@ -23,7 +23,7 @@ namespace OpenQA.Selenium.BiDi.Session; -internal sealed class SessionModule : Module +internal sealed class SessionModule : Module, ISessionModule { private SessionJsonSerializerContext _jsonContext = null!; @@ -58,7 +58,7 @@ public async Task EndAsync(EndOptions? options = null, CancellationTo return await ExecuteCommandAsync(new EndCommand(), options, _jsonContext.EndCommand, _jsonContext.EndResult, cancellationToken).ConfigureAwait(false); } - protected override void Initialize(BiDi bidi, JsonSerializerOptions jsonSerializerOptions) + protected override void Initialize(IBiDi bidi, JsonSerializerOptions jsonSerializerOptions) { jsonSerializerOptions.Converters.Add(new BrowsingContextConverter(bidi)); jsonSerializerOptions.Converters.Add(new BrowserUserContextConverter(bidi)); diff --git a/dotnet/src/webdriver/BiDi/Speculation/ISpeculationModule.cs b/dotnet/src/webdriver/BiDi/Speculation/ISpeculationModule.cs new file mode 100644 index 0000000000000..2cf9c390c7472 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Speculation/ISpeculationModule.cs @@ -0,0 +1,26 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +namespace OpenQA.Selenium.BiDi.Speculation; + +public interface ISpeculationModule +{ + Task OnPrefetchStatusUpdatedAsync(Func handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); + Task OnPrefetchStatusUpdatedAsync(Action handler, SubscriptionOptions? options = null, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/webdriver/BiDi/Speculation/SpeculationBiDiExtensions.cs b/dotnet/src/webdriver/BiDi/Speculation/SpeculationBiDiExtensions.cs index c25f9b7986c0a..3582a9597c0a6 100644 --- a/dotnet/src/webdriver/BiDi/Speculation/SpeculationBiDiExtensions.cs +++ b/dotnet/src/webdriver/BiDi/Speculation/SpeculationBiDiExtensions.cs @@ -21,7 +21,7 @@ namespace OpenQA.Selenium.BiDi.Speculation; public static class SpeculationBiDiExtensions { - public static SpeculationModule AsSpeculation(this BiDi bidi) + public static ISpeculationModule AsSpeculation(this IBiDi bidi) { if (bidi is null) { diff --git a/dotnet/src/webdriver/BiDi/Speculation/SpeculationModule.cs b/dotnet/src/webdriver/BiDi/Speculation/SpeculationModule.cs index f24ec39a899b8..2564aa1afb0dd 100644 --- a/dotnet/src/webdriver/BiDi/Speculation/SpeculationModule.cs +++ b/dotnet/src/webdriver/BiDi/Speculation/SpeculationModule.cs @@ -23,7 +23,7 @@ namespace OpenQA.Selenium.BiDi.Speculation; -public sealed class SpeculationModule : Module +public sealed class SpeculationModule : Module, ISpeculationModule { private SpeculationJsonSerializerContext _jsonContext = null!; @@ -37,7 +37,7 @@ public async Task OnPrefetchStatusUpdatedAsync(Action +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +namespace OpenQA.Selenium.BiDi.Storage; + +public interface IStorageModule +{ + Task DeleteCookiesAsync(DeleteCookiesOptions? options = null, CancellationToken cancellationToken = default); + Task GetCookiesAsync(GetCookiesOptions? options = null, CancellationToken cancellationToken = default); + Task SetCookieAsync(PartialCookie cookie, SetCookieOptions? options = null, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/webdriver/BiDi/Storage/StorageModule.cs b/dotnet/src/webdriver/BiDi/Storage/StorageModule.cs index 6e0784e158813..c4c591e5b5b35 100644 --- a/dotnet/src/webdriver/BiDi/Storage/StorageModule.cs +++ b/dotnet/src/webdriver/BiDi/Storage/StorageModule.cs @@ -23,7 +23,7 @@ namespace OpenQA.Selenium.BiDi.Storage; -public sealed class StorageModule : Module +public sealed class StorageModule : Module, IStorageModule { private StorageJsonSerializerContext _jsonContext = null!; @@ -48,7 +48,7 @@ public async Task SetCookieAsync(PartialCookie cookie, SetCooki return await ExecuteCommandAsync(new SetCookieCommand(@params), options, _jsonContext.SetCookieCommand, _jsonContext.SetCookieResult, cancellationToken).ConfigureAwait(false); } - protected override void Initialize(BiDi bidi, JsonSerializerOptions jsonSerializerOptions) + protected override void Initialize(IBiDi bidi, JsonSerializerOptions jsonSerializerOptions) { jsonSerializerOptions.Converters.Add(new BrowsingContextConverter(bidi)); jsonSerializerOptions.Converters.Add(new BrowserUserContextConverter(bidi)); diff --git a/dotnet/src/webdriver/BiDi/Subscription.cs b/dotnet/src/webdriver/BiDi/Subscription.cs index 52af24c2b4a72..319e346b9cdc0 100644 --- a/dotnet/src/webdriver/BiDi/Subscription.cs +++ b/dotnet/src/webdriver/BiDi/Subscription.cs @@ -21,12 +21,12 @@ namespace OpenQA.Selenium.BiDi; public class Subscription : IAsyncDisposable { - private readonly Broker _broker; + private readonly EventDispatcher _eventDispatcher; - internal Subscription(Session.Subscription subscription, Broker broker, EventHandler eventHandler) + internal Subscription(Session.Subscription subscription, EventDispatcher eventDispatcher, EventHandler eventHandler) { SubscriptionId = subscription; - _broker = broker; + _eventDispatcher = eventDispatcher; EventHandler = eventHandler; } @@ -34,14 +34,15 @@ internal Subscription(Session.Subscription subscription, Broker broker, EventHan internal EventHandler EventHandler { get; } - public async Task UnsubscribeAsync(CancellationToken cancellationToken = default) + public async ValueTask UnsubscribeAsync(CancellationToken cancellationToken = default) { - await _broker.UnsubscribeAsync(this, cancellationToken).ConfigureAwait(false); + await _eventDispatcher.UnsubscribeAsync(this, cancellationToken).ConfigureAwait(false); } public async ValueTask DisposeAsync() { await UnsubscribeAsync().ConfigureAwait(false); + GC.SuppressFinalize(this); } } diff --git a/dotnet/src/webdriver/BiDi/WebDriver.Extensions.cs b/dotnet/src/webdriver/BiDi/WebDriver.Extensions.cs index 2a4c4770729f5..124d6bebd18b6 100644 --- a/dotnet/src/webdriver/BiDi/WebDriver.Extensions.cs +++ b/dotnet/src/webdriver/BiDi/WebDriver.Extensions.cs @@ -21,7 +21,7 @@ namespace OpenQA.Selenium.BiDi; public static class WebDriverExtensions { - public static async Task AsBiDiAsync(this IWebDriver webDriver, BiDiOptions? options = null, CancellationToken cancellationToken = default) + public static async Task AsBiDiAsync(this IWebDriver webDriver, BiDiOptions? options = null, CancellationToken cancellationToken = default) { if (webDriver is null) throw new ArgumentNullException(nameof(webDriver)); diff --git a/dotnet/src/webdriver/BiDi/WebExtension/Extension.cs b/dotnet/src/webdriver/BiDi/WebExtension/Extension.cs index d1186331e28a1..d944c1ef250b2 100644 --- a/dotnet/src/webdriver/BiDi/WebExtension/Extension.cs +++ b/dotnet/src/webdriver/BiDi/WebExtension/Extension.cs @@ -24,7 +24,7 @@ namespace OpenQA.Selenium.BiDi.WebExtension; public sealed record Extension { - public Extension(BiDi bidi, string id) + public Extension(IBiDi bidi, string id) : this(id) { BiDi = bidi ?? throw new ArgumentNullException(nameof(bidi)); @@ -38,10 +38,10 @@ internal Extension(string id) internal string Id { get; } - private BiDi? _bidi; + private IBiDi? _bidi; [JsonIgnore] - public BiDi BiDi + public IBiDi BiDi { get => _bidi ?? throw new InvalidOperationException($"{nameof(BiDi)} instance has not been hydrated."); internal set => _bidi = value; diff --git a/dotnet/src/webdriver/BiDi/WebExtension/IWebExtensionModule.cs b/dotnet/src/webdriver/BiDi/WebExtension/IWebExtensionModule.cs new file mode 100644 index 0000000000000..a3651ba6b90ce --- /dev/null +++ b/dotnet/src/webdriver/BiDi/WebExtension/IWebExtensionModule.cs @@ -0,0 +1,26 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +namespace OpenQA.Selenium.BiDi.WebExtension; + +public interface IWebExtensionModule +{ + Task InstallAsync(ExtensionData extensionData, InstallOptions? options = null, CancellationToken cancellationToken = default); + Task UninstallAsync(Extension extension, UninstallOptions? options = null, CancellationToken cancellationToken = default); +} diff --git a/dotnet/src/webdriver/BiDi/WebExtension/WebExtensionModule.cs b/dotnet/src/webdriver/BiDi/WebExtension/WebExtensionModule.cs index 52482df828f06..42607a30ad5fa 100644 --- a/dotnet/src/webdriver/BiDi/WebExtension/WebExtensionModule.cs +++ b/dotnet/src/webdriver/BiDi/WebExtension/WebExtensionModule.cs @@ -23,7 +23,7 @@ namespace OpenQA.Selenium.BiDi.WebExtension; -public sealed class WebExtensionModule : Module +public sealed class WebExtensionModule : Module, IWebExtensionModule { private WebExtensionJsonSerializerContext _jsonContext = null!; @@ -41,7 +41,7 @@ public async Task UninstallAsync(Extension extension, Uninstall return await ExecuteCommandAsync(new UninstallCommand(@params), options, _jsonContext.UninstallCommand, _jsonContext.UninstallResult, cancellationToken).ConfigureAwait(false); } - protected override void Initialize(BiDi bidi, JsonSerializerOptions jsonSerializerOptions) + protected override void Initialize(IBiDi bidi, JsonSerializerOptions jsonSerializerOptions) { jsonSerializerOptions.Converters.Add(new WebExtensionConverter(bidi)); diff --git a/dotnet/src/webdriver/BiDi/WebSocketTransport.cs b/dotnet/src/webdriver/BiDi/WebSocketTransport.cs index 7619f3ddd81d5..5f9ba5333e8e1 100644 --- a/dotnet/src/webdriver/BiDi/WebSocketTransport.cs +++ b/dotnet/src/webdriver/BiDi/WebSocketTransport.cs @@ -24,17 +24,31 @@ namespace OpenQA.Selenium.BiDi; -sealed class WebSocketTransport(Uri _uri) : ITransport, IDisposable +sealed class WebSocketTransport(ClientWebSocket webSocket) : ITransport, IDisposable { private readonly static ILogger _logger = Internal.Logging.Log.GetLogger(); - private readonly ClientWebSocket _webSocket = new(); + private readonly ClientWebSocket _webSocket = webSocket; private readonly SemaphoreSlim _socketSendSemaphoreSlim = new(1, 1); private readonly MemoryStream _sharedMemoryStream = new(); - public async Task ConnectAsync(CancellationToken cancellationToken) + public static async Task ConnectAsync(Uri uri, CancellationToken cancellationToken) { - await _webSocket.ConnectAsync(_uri, cancellationToken).ConfigureAwait(false); + ClientWebSocket webSocket = new(); + + try + { + await webSocket.ConnectAsync(uri, cancellationToken).ConfigureAwait(false); + } + catch (Exception) + { + webSocket.Dispose(); + throw; + } + + WebSocketTransport webSocketTransport = new(webSocket); + + return webSocketTransport; } public async Task ReceiveAsync(CancellationToken cancellationToken) diff --git a/dotnet/test/common/BiDi/BiDiFixture.cs b/dotnet/test/common/BiDi/BiDiFixture.cs index 8113872a0c3cd..b201612a518bb 100644 --- a/dotnet/test/common/BiDi/BiDiFixture.cs +++ b/dotnet/test/common/BiDi/BiDiFixture.cs @@ -28,7 +28,7 @@ namespace OpenQA.Selenium.BiDi; public class BiDiTestFixture { protected IWebDriver driver; - protected BiDi bidi; + protected IBiDi bidi; protected BrowsingContext.BrowsingContext context; protected UrlBuilder UrlBuilder { get; } = EnvironmentManager.Instance.UrlBuilder;