From cdddb1867f8975d5a3360f580e333071e0596c0f Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:48:09 +0300 Subject: [PATCH 01/25] IBiDi --- dotnet/src/webdriver/BiDi/BiDi.cs | 40 ++++++++++------- .../webdriver/BiDi/Browser/BrowserModule.cs | 2 +- .../webdriver/BiDi/Browser/IBrowserModule.cs | 13 ++++++ .../BrowsingContextInputModule.cs | 2 +- .../BrowsingContextLogModule.cs | 2 +- .../BrowsingContext/BrowsingContextModule.cs | 2 +- .../BrowsingContextNetworkModule.cs | 2 +- .../BrowsingContextScriptModule.cs | 2 +- .../BrowsingContextStorageModule.cs | 2 +- .../BrowsingContext/IBrowsingContextModule.cs | 45 +++++++++++++++++++ .../BiDi/Emulation/EmulationModule.cs | 2 +- .../BiDi/Emulation/IEmulationModule.cs | 17 +++++++ dotnet/src/webdriver/BiDi/IBiDi.cs | 42 +++++++++++++++++ .../src/webdriver/BiDi/Input/IInputModule.cs | 10 +++++ .../src/webdriver/BiDi/Input/InputModule.cs | 2 +- dotnet/src/webdriver/BiDi/Log/ILogModule.cs | 7 +++ dotnet/src/webdriver/BiDi/Log/LogModule.cs | 2 +- .../webdriver/BiDi/Network/INetworkModule.cs | 32 +++++++++++++ .../BiDi/Network/NetworkModule.HighLevel.cs | 2 +- .../webdriver/BiDi/Network/NetworkModule.cs | 2 +- .../BiDi/Permissions/IPermissionModule.cs | 6 +++ .../Permissions/PermissionsBiDiExtensions.cs | 2 +- .../BiDi/Permissions/PermissionsModule.cs | 2 +- .../webdriver/BiDi/Script/IScriptModule.cs | 21 +++++++++ .../src/webdriver/BiDi/Script/ScriptModule.cs | 2 +- .../webdriver/BiDi/Session/ISessionModule.cs | 10 +++++ .../webdriver/BiDi/Session/SessionModule.cs | 2 +- .../BiDi/Speculation/ISpeculationModule.cs | 7 +++ .../Speculation/SpeculationBiDiExtensions.cs | 2 +- .../BiDi/Speculation/SpeculationModule.cs | 2 +- .../webdriver/BiDi/Storage/IStorageModule.cs | 8 ++++ .../webdriver/BiDi/Storage/StorageModule.cs | 2 +- .../webdriver/BiDi/WebDriver.Extensions.cs | 2 +- .../BiDi/WebExtension/IWebExtensionModule.cs | 7 +++ .../BiDi/WebExtension/WebExtensionModule.cs | 2 +- dotnet/test/common/BiDi/BiDiFixture.cs | 2 +- 36 files changed, 272 insertions(+), 37 deletions(-) create mode 100644 dotnet/src/webdriver/BiDi/Browser/IBrowserModule.cs create mode 100644 dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs create mode 100644 dotnet/src/webdriver/BiDi/Emulation/IEmulationModule.cs create mode 100644 dotnet/src/webdriver/BiDi/IBiDi.cs create mode 100644 dotnet/src/webdriver/BiDi/Input/IInputModule.cs create mode 100644 dotnet/src/webdriver/BiDi/Log/ILogModule.cs create mode 100644 dotnet/src/webdriver/BiDi/Network/INetworkModule.cs create mode 100644 dotnet/src/webdriver/BiDi/Permissions/IPermissionModule.cs create mode 100644 dotnet/src/webdriver/BiDi/Script/IScriptModule.cs create mode 100644 dotnet/src/webdriver/BiDi/Session/ISessionModule.cs create mode 100644 dotnet/src/webdriver/BiDi/Speculation/ISpeculationModule.cs create mode 100644 dotnet/src/webdriver/BiDi/Storage/IStorageModule.cs create mode 100644 dotnet/src/webdriver/BiDi/WebExtension/IWebExtensionModule.cs diff --git a/dotnet/src/webdriver/BiDi/BiDi.cs b/dotnet/src/webdriver/BiDi/BiDi.cs index 510dab74dd180..1090ded3b3c87 100644 --- a/dotnet/src/webdriver/BiDi/BiDi.cs +++ b/dotnet/src/webdriver/BiDi/BiDi.cs @@ -20,11 +20,21 @@ using System.Collections.Concurrent; using System.Text.Json; using System.Text.Json.Serialization; +using OpenQA.Selenium.BiDi.Browser; +using OpenQA.Selenium.BiDi.BrowsingContext; +using OpenQA.Selenium.BiDi.Emulation; +using OpenQA.Selenium.BiDi.Input; using OpenQA.Selenium.BiDi.Json.Converters; +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 sealed class BiDi : IAsyncDisposable +public sealed class BiDi : IBiDi { private readonly ConcurrentDictionary _modules = new(); @@ -37,27 +47,27 @@ private BiDi(string url) private Broker Broker { get; } - internal Session.SessionModule SessionModule => AsModule(); + internal ISessionModule SessionModule => AsModule(); - public BrowsingContext.BrowsingContextModule BrowsingContext => AsModule(); + public IBrowsingContextModule BrowsingContext => AsModule(); - public Browser.BrowserModule Browser => AsModule(); + public IBrowserModule Browser => AsModule(); - public Network.NetworkModule Network => AsModule(); + public INetworkModule Network => AsModule(); - public Input.InputModule Input => AsModule(); + public IInputModule Input => AsModule(); - public Script.ScriptModule Script => AsModule(); + public IScriptModule Script => AsModule(); - public Log.LogModule Log => AsModule(); + public ILogModule Log => AsModule(); - public Storage.StorageModule Storage => AsModule(); + public IStorageModule Storage => AsModule(); - public WebExtension.WebExtensionModule WebExtension => AsModule(); + public IWebExtensionModule WebExtension => AsModule(); - public Emulation.EmulationModule Emulation => AsModule(); + public 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); @@ -66,17 +76,17 @@ public static async Task ConnectAsync(string url, BiDiOptions? options = n 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); } - 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); } - public Task EndAsync(Session.EndOptions? options = null, CancellationToken cancellationToken = default) + public Task EndAsync(EndOptions? options = null, CancellationToken cancellationToken = default) { return SessionModule.EndAsync(options, cancellationToken); } diff --git a/dotnet/src/webdriver/BiDi/Browser/BrowserModule.cs b/dotnet/src/webdriver/BiDi/Browser/BrowserModule.cs index 19bce5cabdd77..8f4fa91d382f3 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!; diff --git a/dotnet/src/webdriver/BiDi/Browser/IBrowserModule.cs b/dotnet/src/webdriver/BiDi/Browser/IBrowserModule.cs new file mode 100644 index 0000000000000..9a5495bdc9456 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Browser/IBrowserModule.cs @@ -0,0 +1,13 @@ +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/BrowsingContext/BrowsingContextInputModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextInputModule.cs index 0b7b9b526177a..cafa1cad903d9 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) { 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..c0ff1cc2e6ff7 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) { 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..857549e715c7c 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!; diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextNetworkModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextNetworkModule.cs index 217de2398ab94..7961e4b422065 100644 --- a/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextNetworkModule.cs +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextNetworkModule.cs @@ -21,7 +21,7 @@ namespace OpenQA.Selenium.BiDi.BrowsingContext; -public sealed class BrowsingContextNetworkModule(BrowsingContext context, NetworkModule networkModule) +public sealed class BrowsingContextNetworkModule(BrowsingContext context, INetworkModule networkModule) { public async Task 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..2ff2cf297547e 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) { 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..41eb57e8f5707 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) { public Task GetCookiesAsync(ContextGetCookiesOptions? 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..c766b1a3901bc --- /dev/null +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs @@ -0,0 +1,45 @@ +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/Emulation/EmulationModule.cs b/dotnet/src/webdriver/BiDi/Emulation/EmulationModule.cs index 476e9178de5a3..05d4f0b387223 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!; diff --git a/dotnet/src/webdriver/BiDi/Emulation/IEmulationModule.cs b/dotnet/src/webdriver/BiDi/Emulation/IEmulationModule.cs new file mode 100644 index 0000000000000..18482f0b3e4c3 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Emulation/IEmulationModule.cs @@ -0,0 +1,17 @@ +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/IBiDi.cs b/dotnet/src/webdriver/BiDi/IBiDi.cs new file mode 100644 index 0000000000000..d588bde3ccb7b --- /dev/null +++ b/dotnet/src/webdriver/BiDi/IBiDi.cs @@ -0,0 +1,42 @@ + +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); + + T AsModule() where T : Module, new(); +} diff --git a/dotnet/src/webdriver/BiDi/Input/IInputModule.cs b/dotnet/src/webdriver/BiDi/Input/IInputModule.cs new file mode 100644 index 0000000000000..febb4479c5b97 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Input/IInputModule.cs @@ -0,0 +1,10 @@ +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..5b1108b893644 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!; diff --git a/dotnet/src/webdriver/BiDi/Log/ILogModule.cs b/dotnet/src/webdriver/BiDi/Log/ILogModule.cs new file mode 100644 index 0000000000000..d0f263f9dcd27 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Log/ILogModule.cs @@ -0,0 +1,7 @@ +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..c73f9e4ac3971 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!; diff --git a/dotnet/src/webdriver/BiDi/Network/INetworkModule.cs b/dotnet/src/webdriver/BiDi/Network/INetworkModule.cs new file mode 100644 index 0000000000000..123a4e95ab9f6 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Network/INetworkModule.cs @@ -0,0 +1,32 @@ +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/NetworkModule.HighLevel.cs b/dotnet/src/webdriver/BiDi/Network/NetworkModule.HighLevel.cs index 4980c7e348a82..947033379b1a0 100644 --- a/dotnet/src/webdriver/BiDi/Network/NetworkModule.HighLevel.cs +++ b/dotnet/src/webdriver/BiDi/Network/NetworkModule.HighLevel.cs @@ -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..f372b9d7f00fc 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!; diff --git a/dotnet/src/webdriver/BiDi/Permissions/IPermissionModule.cs b/dotnet/src/webdriver/BiDi/Permissions/IPermissionModule.cs new file mode 100644 index 0000000000000..4166d6abb3e1d --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Permissions/IPermissionModule.cs @@ -0,0 +1,6 @@ +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..34c75c3e6cc0d 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!; diff --git a/dotnet/src/webdriver/BiDi/Script/IScriptModule.cs b/dotnet/src/webdriver/BiDi/Script/IScriptModule.cs new file mode 100644 index 0000000000000..0399c12d8fe80 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Script/IScriptModule.cs @@ -0,0 +1,21 @@ +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/ScriptModule.cs b/dotnet/src/webdriver/BiDi/Script/ScriptModule.cs index 59b98dfe6904d..5dd6da1a05281 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!; diff --git a/dotnet/src/webdriver/BiDi/Session/ISessionModule.cs b/dotnet/src/webdriver/BiDi/Session/ISessionModule.cs new file mode 100644 index 0000000000000..814942a96eff1 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Session/ISessionModule.cs @@ -0,0 +1,10 @@ +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..749dc11e25e99 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!; diff --git a/dotnet/src/webdriver/BiDi/Speculation/ISpeculationModule.cs b/dotnet/src/webdriver/BiDi/Speculation/ISpeculationModule.cs new file mode 100644 index 0000000000000..29717b163ebee --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Speculation/ISpeculationModule.cs @@ -0,0 +1,7 @@ +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..9590a69ae0cbc 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!; diff --git a/dotnet/src/webdriver/BiDi/Storage/IStorageModule.cs b/dotnet/src/webdriver/BiDi/Storage/IStorageModule.cs new file mode 100644 index 0000000000000..7cbf6f2af9470 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Storage/IStorageModule.cs @@ -0,0 +1,8 @@ +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..4480be7fecc2c 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!; 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/IWebExtensionModule.cs b/dotnet/src/webdriver/BiDi/WebExtension/IWebExtensionModule.cs new file mode 100644 index 0000000000000..d6c3f909d1000 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/WebExtension/IWebExtensionModule.cs @@ -0,0 +1,7 @@ +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..d024af2cb7b2b 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!; 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; From 40a888e456870faf63f59b336b31633b23a9061e Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sat, 21 Feb 2026 16:51:57 +0300 Subject: [PATCH 02/25] Interface in event args --- dotnet/src/webdriver/BiDi/EventArgs.cs | 4 ++-- .../src/webdriver/BiDi/Network/NetworkModule.HighLevel.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) 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/Network/NetworkModule.HighLevel.cs b/dotnet/src/webdriver/BiDi/Network/NetworkModule.HighLevel.cs index 947033379b1a0..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; From 7a33df8f187f331af24d213b45dc112cd200819d Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sat, 21 Feb 2026 17:23:23 +0300 Subject: [PATCH 03/25] Session --- dotnet/src/webdriver/BiDi/BiDi.cs | 8 ++++---- dotnet/src/webdriver/BiDi/Broker.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/BiDi.cs b/dotnet/src/webdriver/BiDi/BiDi.cs index 1090ded3b3c87..0376179f5aae3 100644 --- a/dotnet/src/webdriver/BiDi/BiDi.cs +++ b/dotnet/src/webdriver/BiDi/BiDi.cs @@ -47,7 +47,7 @@ private BiDi(string url) private Broker Broker { get; } - internal ISessionModule SessionModule => AsModule(); + internal ISessionModule Session => AsModule(); public IBrowsingContextModule BrowsingContext => AsModule(); @@ -78,17 +78,17 @@ public static async Task ConnectAsync(string url, BiDiOptions? options = public Task StatusAsync(StatusOptions? options = null, CancellationToken cancellationToken = default) { - return SessionModule.StatusAsync(options, cancellationToken); + return Session.StatusAsync(options, cancellationToken); } 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(EndOptions? options = null, CancellationToken cancellationToken = default) { - return SessionModule.EndAsync(options, cancellationToken); + return Session.EndAsync(options, cancellationToken); } public async ValueTask DisposeAsync() diff --git a/dotnet/src/webdriver/BiDi/Broker.cs b/dotnet/src/webdriver/BiDi/Broker.cs index 8e3b8ec8c1e5d..7c65df3d3dcc1 100644 --- a/dotnet/src/webdriver/BiDi/Broker.cs +++ b/dotnet/src/webdriver/BiDi/Broker.cs @@ -164,7 +164,7 @@ public async Task SubscribeAsync(string eventName, Eve var handlers = _eventHandlers.GetOrAdd(eventName, (a) => []); - var subscribeResult = await _bidi.SessionModule.SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken).ConfigureAwait(false); + var subscribeResult = await _bidi.Session.SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken).ConfigureAwait(false); handlers.Add(eventHandler); @@ -177,7 +177,7 @@ public async Task UnsubscribeAsync(Subscription subscription, CancellationToken eventHandlers.Remove(subscription.EventHandler); - await _bidi.SessionModule.UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false); + await _bidi.Session.UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false); } public async ValueTask DisposeAsync() From 0e219701c9bdf153b207cf03360da62102a73c87 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 17:13:21 +0300 Subject: [PATCH 04/25] EventDispatcher --- dotnet/src/webdriver/BiDi/BiDi.cs | 14 +- dotnet/src/webdriver/BiDi/Broker.cs | 113 ++++------------ dotnet/src/webdriver/BiDi/EventDispatcher.cs | 134 +++++++++++++++++++ dotnet/src/webdriver/BiDi/Subscription.cs | 8 +- 4 files changed, 173 insertions(+), 96 deletions(-) create mode 100644 dotnet/src/webdriver/BiDi/EventDispatcher.cs diff --git a/dotnet/src/webdriver/BiDi/BiDi.cs b/dotnet/src/webdriver/BiDi/BiDi.cs index 0376179f5aae3..611e90128995b 100644 --- a/dotnet/src/webdriver/BiDi/BiDi.cs +++ b/dotnet/src/webdriver/BiDi/BiDi.cs @@ -41,11 +41,11 @@ public sealed class BiDi : IBiDi private BiDi(string url) { var uri = new Uri(url); - - Broker = new Broker(this, uri); } - private Broker Broker { get; } + private Broker Broker { get; set; } = null!; + + private EventDispatcher EventDispatcher { get; set; } = null!; internal ISessionModule Session => AsModule(); @@ -71,7 +71,12 @@ public static async Task ConnectAsync(string url, BiDiOptions? options = { var bidi = new BiDi(url); - await bidi.Broker.ConnectAsync(cancellationToken).ConfigureAwait(false); + var eventDispatcher = new EventDispatcher(bidi.Session, () => bidi); + + var broker = await Broker.CreateAsync(new Uri(url), eventDispatcher, cancellationToken).ConfigureAwait(false); + + bidi.Broker = broker; + bidi.EventDispatcher = eventDispatcher; return bidi; } @@ -93,6 +98,7 @@ public Task EndAsync(EndOptions? options = null, CancellationToken cancellationT public async ValueTask DisposeAsync() { + await EventDispatcher.DisposeAsync().ConfigureAwait(false); await Broker.DisposeAsync().ConfigureAwait(false); GC.SuppressFinalize(this); } diff --git a/dotnet/src/webdriver/BiDi/Broker.cs b/dotnet/src/webdriver/BiDi/Broker.cs index 7c65df3d3dcc1..0898532d0209b 100644 --- a/dotnet/src/webdriver/BiDi/Broker.cs +++ b/dotnet/src/webdriver/BiDi/Broker.cs @@ -20,7 +20,6 @@ using System.Collections.Concurrent; using System.Text.Json; using System.Text.Json.Serialization.Metadata; -using System.Threading.Channels; using OpenQA.Selenium.Internal.Logging; namespace OpenQA.Selenium.BiDi; @@ -29,40 +28,40 @@ 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 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; + + private Broker(ITransport transport, EventDispatcher eventDispatcher) + { + _transport = transport; + _eventDispatcher = eventDispatcher; + + _receiveMessagesCancellationTokenSource = new CancellationTokenSource(); + _receivingMessageTask = _myTaskFactory.StartNew(async () => await ReceiveMessagesAsync(_receiveMessagesCancellationTokenSource.Token), TaskCreationOptions.LongRunning).Unwrap(); + } - internal Broker(BiDi bidi, Uri url) + public Task SubscribeAsync(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken) + where TEventArgs : EventArgs { - _bidi = bidi; - _transport = new WebSocketTransport(url); + return _eventDispatcher.SubscribeAsync(eventName, eventHandler, options, jsonTypeInfo, cancellationToken); } - public async Task ConnectAsync(CancellationToken cancellationToken) + public static async Task CreateAsync(Uri url, EventDispatcher eventDispatcher, CancellationToken cancellationToken) { - await _transport.ConnectAsync(cancellationToken).ConfigureAwait(false); + var transport = new WebSocketTransport(url); + + await transport.ConnectAsync(cancellationToken).ConfigureAwait(false); - _receiveMessagesCancellationTokenSource = new CancellationTokenSource(); - _receivingMessageTask = _myTaskFactory.StartNew(async () => await ReceiveMessagesAsync(_receiveMessagesCancellationTokenSource.Token), TaskCreationOptions.LongRunning).Unwrap(); - _eventEmitterTask = _myTaskFactory.StartNew(ProcessEventsAwaiterAsync).Unwrap(); + return new Broker(transport, eventDispatcher); } private async Task ReceiveMessagesAsync(CancellationToken cancellationToken) @@ -97,40 +96,7 @@ private async Task ReceiveMessagesAsync(CancellationToken cancellationToken) } } - private async Task ProcessEventsAwaiterAsync() - { - 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}"); - } - } - } - } - } public async Task ExecuteCommandAsync(TCommand command, CommandOptions? options, JsonTypeInfo jsonCommandTypeInfo, JsonTypeInfo jsonResultTypeInfo, CancellationToken cancellationToken) where TCommand : Command @@ -157,39 +123,14 @@ 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.Session.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.Session.UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false); - } public async ValueTask DisposeAsync() { - _pendingEvents.Writer.Complete(); + _receiveMessagesCancellationTokenSource.Cancel(); + _receiveMessagesCancellationTokenSource.Dispose(); - _receiveMessagesCancellationTokenSource?.Cancel(); - - if (_eventEmitterTask is not null) - { - await _eventEmitterTask.ConfigureAwait(false); - } + await _receivingMessageTask.ConfigureAwait(false); _transport.Dispose(); @@ -284,13 +225,11 @@ private void ProcessReceivedMessage(byte[]? data) 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)) + if (_eventDispatcher.TryGetEventTypeInfo(method, out var eventInfo) && eventInfo is not null) { var eventArgs = (EventArgs)JsonSerializer.Deserialize(ref paramsReader, eventInfo)!; - eventArgs.BiDi = _bidi; - - _pendingEvents.Writer.TryWrite(new EventInfo(method, eventArgs)); + _eventDispatcher.EnqueueEvent(method, eventArgs); } else { @@ -317,6 +256,4 @@ private void ProcessReceivedMessage(byte[]? data) } private readonly record struct CommandInfo(TaskCompletionSource TaskCompletionSource, JsonTypeInfo JsonResultTypeInfo); - - private readonly record struct EventInfo(string Method, EventArgs Params); } diff --git a/dotnet/src/webdriver/BiDi/EventDispatcher.cs b/dotnet/src/webdriver/BiDi/EventDispatcher.cs new file mode 100644 index 0000000000000..77446d9c01f33 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/EventDispatcher.cs @@ -0,0 +1,134 @@ +// +// 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.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 ISessionModule _session; + private readonly Func _bidiProvider; + + private readonly ConcurrentDictionary> _eventHandlers = new(); + private readonly Dictionary _eventTypesMap = []; + + 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(ISessionModule session, Func bidiProvider) + { + _session = session; + _bidiProvider = bidiProvider; + + _eventEmitterTask = _myTaskFactory.StartNew(ProcessEventsAwaiterAsync).Unwrap(); + } + + 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 _session.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 _session.UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false); + } + + internal void EnqueueEvent(string method, EventArgs eventArgs) + { + eventArgs.BiDi = _bidiProvider(); + + _pendingEvents.Writer.TryWrite(new EventInfo(method, eventArgs)); + } + + internal bool TryGetEventTypeInfo(string method, out JsonTypeInfo? jsonTypeInfo) + { + return _eventTypesMap.TryGetValue(method, out jsonTypeInfo); + } + + private async Task ProcessEventsAwaiterAsync() + { + 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; + + await handler.InvokeAsync(args).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 EventInfo(string Method, EventArgs Params); +} diff --git a/dotnet/src/webdriver/BiDi/Subscription.cs b/dotnet/src/webdriver/BiDi/Subscription.cs index 52af24c2b4a72..4e31e6bb5a3a9 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; } @@ -36,7 +36,7 @@ internal Subscription(Session.Subscription subscription, Broker broker, EventHan public async Task UnsubscribeAsync(CancellationToken cancellationToken = default) { - await _broker.UnsubscribeAsync(this, cancellationToken).ConfigureAwait(false); + await _eventDispatcher.UnsubscribeAsync(this, cancellationToken).ConfigureAwait(false); } public async ValueTask DisposeAsync() From fd4a896e259435f2562c07fd9bf3af3f3d72b230 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 18:53:46 +0300 Subject: [PATCH 05/25] Expose interface --- dotnet/src/webdriver/BiDi/Broker.cs | 74 +++++++++---------- .../webdriver/BiDi/Browser/BrowserModule.cs | 2 +- .../src/webdriver/BiDi/Browser/UserContext.cs | 6 +- .../BiDi/BrowsingContext/BrowsingContext.cs | 4 +- .../BrowsingContext/BrowsingContextModule.cs | 2 +- .../BiDi/Emulation/EmulationModule.cs | 2 +- .../src/webdriver/BiDi/Input/InputModule.cs | 2 +- .../Converters/BrowserUserContextConverter.cs | 2 +- .../Converters/BrowsingContextConverter.cs | 9 +-- .../Json/Converters/CollectorConverter.cs | 2 +- .../BiDi/Json/Converters/HandleConverter.cs | 2 +- .../Json/Converters/InterceptConverter.cs | 2 +- .../Json/Converters/InternalIdConverter.cs | 2 +- .../Json/Converters/PreloadScriptConverter.cs | 2 +- .../BiDi/Json/Converters/RealmConverter.cs | 2 +- .../Json/Converters/WebExtensionConverter.cs | 2 +- dotnet/src/webdriver/BiDi/Log/LogModule.cs | 2 +- dotnet/src/webdriver/BiDi/Module.cs | 4 +- .../src/webdriver/BiDi/Network/Collector.cs | 6 +- .../src/webdriver/BiDi/Network/Intercept.cs | 6 +- .../webdriver/BiDi/Network/NetworkModule.cs | 2 +- .../BiDi/Permissions/PermissionsModule.cs | 2 +- dotnet/src/webdriver/BiDi/Script/Handle.cs | 6 +- .../src/webdriver/BiDi/Script/InternalId.cs | 6 +- .../webdriver/BiDi/Script/PreloadScript.cs | 6 +- dotnet/src/webdriver/BiDi/Script/Realm.cs | 6 +- .../src/webdriver/BiDi/Script/ScriptModule.cs | 2 +- .../webdriver/BiDi/Session/SessionModule.cs | 2 +- .../BiDi/Speculation/SpeculationModule.cs | 2 +- .../webdriver/BiDi/Storage/StorageModule.cs | 2 +- .../webdriver/BiDi/WebExtension/Extension.cs | 6 +- .../BiDi/WebExtension/WebExtensionModule.cs | 2 +- 32 files changed, 87 insertions(+), 92 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/Broker.cs b/dotnet/src/webdriver/BiDi/Broker.cs index 0898532d0209b..03a1fe1942bae 100644 --- a/dotnet/src/webdriver/BiDi/Broker.cs +++ b/dotnet/src/webdriver/BiDi/Broker.cs @@ -49,52 +49,22 @@ private Broker(ITransport transport, EventDispatcher eventDispatcher) _receivingMessageTask = _myTaskFactory.StartNew(async () => await ReceiveMessagesAsync(_receiveMessagesCancellationTokenSource.Token), TaskCreationOptions.LongRunning).Unwrap(); } - public Task SubscribeAsync(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken) - where TEventArgs : EventArgs - { - return _eventDispatcher.SubscribeAsync(eventName, eventHandler, options, jsonTypeInfo, cancellationToken); - } - public static async Task CreateAsync(Uri url, EventDispatcher eventDispatcher, CancellationToken cancellationToken) { var transport = new WebSocketTransport(url); - + await transport.ConnectAsync(cancellationToken).ConfigureAwait(false); return new Broker(transport, eventDispatcher); } - private async Task ReceiveMessagesAsync(CancellationToken cancellationToken) + public Task SubscribeAsync(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken) + where TEventArgs : EventArgs { - try - { - while (!cancellationToken.IsCancellationRequested) - { - var data = await _transport.ReceiveAsync(cancellationToken).ConfigureAwait(false); + return _eventDispatcher.SubscribeAsync(eventName, eventHandler, options, jsonTypeInfo, cancellationToken); + } - 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; - } - } @@ -123,8 +93,6 @@ public async Task ExecuteCommandAsync(TCommand comma return (TResult)await tcs.Task.ConfigureAwait(false); } - - public async ValueTask DisposeAsync() { _receiveMessagesCancellationTokenSource.Cancel(); @@ -255,5 +223,37 @@ private void ProcessReceivedMessage(byte[]? data) } } + 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 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 8f4fa91d382f3..4885e720a512c 100644 --- a/dotnet/src/webdriver/BiDi/Browser/BrowserModule.cs +++ b/dotnet/src/webdriver/BiDi/Browser/BrowserModule.cs @@ -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/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..80d6221214626 100644 --- a/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs @@ -44,10 +44,10 @@ internal BrowsingContext(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/BrowsingContextModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextModule.cs index 857549e715c7c..08b5bc39cbcdf 100644 --- a/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextModule.cs +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextModule.cs @@ -251,7 +251,7 @@ public async Task OnUserPromptClosedAsync(Action 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/Input/InputModule.cs b/dotnet/src/webdriver/BiDi/Input/InputModule.cs index 5b1108b893644..f6c72fd81952a 100644 --- a/dotnet/src/webdriver/BiDi/Input/InputModule.cs +++ b/dotnet/src/webdriver/BiDi/Input/InputModule.cs @@ -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/LogModule.cs b/dotnet/src/webdriver/BiDi/Log/LogModule.cs index c73f9e4ac3971..66cb735dce597 100644 --- a/dotnet/src/webdriver/BiDi/Log/LogModule.cs +++ b/dotnet/src/webdriver/BiDi/Log/LogModule.cs @@ -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/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.cs b/dotnet/src/webdriver/BiDi/Network/NetworkModule.cs index f372b9d7f00fc..d9f7745dbb3b8 100644 --- a/dotnet/src/webdriver/BiDi/Network/NetworkModule.cs +++ b/dotnet/src/webdriver/BiDi/Network/NetworkModule.cs @@ -171,7 +171,7 @@ public async Task OnAuthRequiredAsync(Action 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/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 5dd6da1a05281..eaf2f6f26dcdf 100644 --- a/dotnet/src/webdriver/BiDi/Script/ScriptModule.cs +++ b/dotnet/src/webdriver/BiDi/Script/ScriptModule.cs @@ -114,7 +114,7 @@ public async Task OnRealmDestroyedAsync(Action 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/SpeculationModule.cs b/dotnet/src/webdriver/BiDi/Speculation/SpeculationModule.cs index 9590a69ae0cbc..2564aa1afb0dd 100644 --- a/dotnet/src/webdriver/BiDi/Speculation/SpeculationModule.cs +++ b/dotnet/src/webdriver/BiDi/Speculation/SpeculationModule.cs @@ -37,7 +37,7 @@ public async Task OnPrefetchStatusUpdatedAsync(Action 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/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/WebExtensionModule.cs b/dotnet/src/webdriver/BiDi/WebExtension/WebExtensionModule.cs index d024af2cb7b2b..42607a30ad5fa 100644 --- a/dotnet/src/webdriver/BiDi/WebExtension/WebExtensionModule.cs +++ b/dotnet/src/webdriver/BiDi/WebExtension/WebExtensionModule.cs @@ -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)); From 5f0d7769f3e2270f700f1ea85e8dc914bfed8cf7 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 19:05:36 +0300 Subject: [PATCH 06/25] BrowsingContext interfaces --- .../BiDi/BrowsingContext/BrowsingContext.cs | 22 +++++++++---------- .../BrowsingContextInputModule.cs | 2 +- .../BrowsingContextLogModule.cs | 2 +- .../BrowsingContextNetworkModule.cs | 2 +- .../BrowsingContextScriptModule.cs | 2 +- .../BrowsingContextStorageModule.cs | 2 +- .../IBrowsingContextInputModule.cs | 12 ++++++++++ .../IBrowsingContextLogModule.cs | 9 ++++++++ .../IBrowsingContextNetworkModule.cs | 22 +++++++++++++++++++ .../IBrowsingContextScriptModule.cs | 14 ++++++++++++ .../IBrowsingContextStorageModule.cs | 10 +++++++++ 11 files changed, 83 insertions(+), 16 deletions(-) create mode 100644 dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextInputModule.cs create mode 100644 dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextLogModule.cs create mode 100644 dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextNetworkModule.cs create mode 100644 dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextScriptModule.cs create mode 100644 dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextStorageModule.cs diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs index 80d6221214626..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,11 +36,11 @@ 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; } @@ -54,19 +54,19 @@ public IBiDi BiDi } [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 cafa1cad903d9..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, IInputModule 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 c0ff1cc2e6ff7..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, ILogModule 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/BrowsingContextNetworkModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextNetworkModule.cs index 7961e4b422065..324b0729336c8 100644 --- a/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextNetworkModule.cs +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextNetworkModule.cs @@ -21,7 +21,7 @@ namespace OpenQA.Selenium.BiDi.BrowsingContext; -public sealed class BrowsingContextNetworkModule(BrowsingContext context, INetworkModule networkModule) +public sealed class BrowsingContextNetworkModule(BrowsingContext context, INetworkModule networkModule) : IBrowsingContextNetworkModule { public async Task 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 2ff2cf297547e..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, IScriptModule 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 41eb57e8f5707..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, IStorageModule 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..87bf1dfd9be4c --- /dev/null +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextInputModule.cs @@ -0,0 +1,12 @@ +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..6b650f3381da0 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextLogModule.cs @@ -0,0 +1,9 @@ +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/IBrowsingContextNetworkModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextNetworkModule.cs new file mode 100644 index 0000000000000..fa6d2b93cad65 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextNetworkModule.cs @@ -0,0 +1,22 @@ +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..43bb68c0bf282 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextScriptModule.cs @@ -0,0 +1,14 @@ +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..29076f7388276 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextStorageModule.cs @@ -0,0 +1,10 @@ +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); +} From c697469037a6eeca5b93aeef5dd3e3ca1f6a6848 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 19:09:45 +0300 Subject: [PATCH 07/25] Hide implementation --- dotnet/src/webdriver/BiDi/BiDi.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/BiDi.cs b/dotnet/src/webdriver/BiDi/BiDi.cs index 611e90128995b..a7333655b0afe 100644 --- a/dotnet/src/webdriver/BiDi/BiDi.cs +++ b/dotnet/src/webdriver/BiDi/BiDi.cs @@ -34,15 +34,10 @@ namespace OpenQA.Selenium.BiDi; -public sealed class BiDi : IBiDi +internal sealed class BiDi : IBiDi { private readonly ConcurrentDictionary _modules = new(); - private BiDi(string url) - { - var uri = new Uri(url); - } - private Broker Broker { get; set; } = null!; private EventDispatcher EventDispatcher { get; set; } = null!; @@ -69,11 +64,11 @@ private BiDi(string url) public static async Task ConnectAsync(string url, BiDiOptions? options = null, CancellationToken cancellationToken = default) { - var bidi = new BiDi(url); + BiDi bidi = new(); - var eventDispatcher = new EventDispatcher(bidi.Session, () => bidi); + EventDispatcher eventDispatcher = new(bidi.Session, () => bidi); - var broker = await Broker.CreateAsync(new Uri(url), eventDispatcher, cancellationToken).ConfigureAwait(false); + Broker broker = await Broker.CreateAsync(new Uri(url), eventDispatcher, cancellationToken).ConfigureAwait(false); bidi.Broker = broker; bidi.EventDispatcher = eventDispatcher; From ed0a2563de6643db5a3045787c67324a229f21df Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 19:32:04 +0300 Subject: [PATCH 08/25] License headers --- .../webdriver/BiDi/Browser/IBrowserModule.cs | 19 ++++++++++++++ .../IBrowsingContextInputModule.cs | 19 ++++++++++++++ .../IBrowsingContextLogModule.cs | 19 ++++++++++++++ .../BrowsingContext/IBrowsingContextModule.cs | 19 ++++++++++++++ .../IBrowsingContextNetworkModule.cs | 19 ++++++++++++++ .../IBrowsingContextScriptModule.cs | 19 ++++++++++++++ .../IBrowsingContextStorageModule.cs | 19 ++++++++++++++ .../BiDi/Emulation/IEmulationModule.cs | 19 ++++++++++++++ dotnet/src/webdriver/BiDi/IBiDi.cs | 19 ++++++++++++++ .../src/webdriver/BiDi/Input/IInputModule.cs | 19 ++++++++++++++ dotnet/src/webdriver/BiDi/Log/ILogModule.cs | 19 ++++++++++++++ .../webdriver/BiDi/Network/INetworkModule.cs | 19 ++++++++++++++ .../BiDi/Permissions/IPermissionModule.cs | 6 ----- .../BiDi/Permissions/IPermissionsModule.cs | 25 +++++++++++++++++++ .../webdriver/BiDi/Script/IScriptModule.cs | 19 ++++++++++++++ .../webdriver/BiDi/Session/ISessionModule.cs | 19 ++++++++++++++ .../BiDi/Speculation/ISpeculationModule.cs | 19 ++++++++++++++ .../webdriver/BiDi/Storage/IStorageModule.cs | 19 ++++++++++++++ .../BiDi/WebExtension/IWebExtensionModule.cs | 19 ++++++++++++++ 19 files changed, 348 insertions(+), 6 deletions(-) delete mode 100644 dotnet/src/webdriver/BiDi/Permissions/IPermissionModule.cs create mode 100644 dotnet/src/webdriver/BiDi/Permissions/IPermissionsModule.cs diff --git a/dotnet/src/webdriver/BiDi/Browser/IBrowserModule.cs b/dotnet/src/webdriver/BiDi/Browser/IBrowserModule.cs index 9a5495bdc9456..b79395b311e81 100644 --- a/dotnet/src/webdriver/BiDi/Browser/IBrowserModule.cs +++ b/dotnet/src/webdriver/BiDi/Browser/IBrowserModule.cs @@ -1,3 +1,22 @@ +// +// 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 diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextInputModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextInputModule.cs index 87bf1dfd9be4c..ac76063ca471c 100644 --- a/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextInputModule.cs +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextInputModule.cs @@ -1,3 +1,22 @@ +// +// 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; diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextLogModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextLogModule.cs index 6b650f3381da0..74d5c0f22d90e 100644 --- a/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextLogModule.cs +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextLogModule.cs @@ -1,3 +1,22 @@ +// +// 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; diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs index c766b1a3901bc..bd99fb34474b1 100644 --- a/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextModule.cs @@ -1,3 +1,22 @@ +// +// 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 diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextNetworkModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextNetworkModule.cs index fa6d2b93cad65..11be308511e66 100644 --- a/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextNetworkModule.cs +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextNetworkModule.cs @@ -1,3 +1,22 @@ +// +// 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; diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextScriptModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextScriptModule.cs index 43bb68c0bf282..94885ef7af75c 100644 --- a/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextScriptModule.cs +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextScriptModule.cs @@ -1,3 +1,22 @@ +// +// 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; diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextStorageModule.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextStorageModule.cs index 29076f7388276..19e18ee6b3060 100644 --- a/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextStorageModule.cs +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/IBrowsingContextStorageModule.cs @@ -1,3 +1,22 @@ +// +// 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; diff --git a/dotnet/src/webdriver/BiDi/Emulation/IEmulationModule.cs b/dotnet/src/webdriver/BiDi/Emulation/IEmulationModule.cs index 18482f0b3e4c3..3b157b0876356 100644 --- a/dotnet/src/webdriver/BiDi/Emulation/IEmulationModule.cs +++ b/dotnet/src/webdriver/BiDi/Emulation/IEmulationModule.cs @@ -1,3 +1,22 @@ +// +// 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 diff --git a/dotnet/src/webdriver/BiDi/IBiDi.cs b/dotnet/src/webdriver/BiDi/IBiDi.cs index d588bde3ccb7b..2f58204055c2a 100644 --- a/dotnet/src/webdriver/BiDi/IBiDi.cs +++ b/dotnet/src/webdriver/BiDi/IBiDi.cs @@ -1,4 +1,23 @@ +// +// 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.Browser; using OpenQA.Selenium.BiDi.BrowsingContext; using OpenQA.Selenium.BiDi.Emulation; diff --git a/dotnet/src/webdriver/BiDi/Input/IInputModule.cs b/dotnet/src/webdriver/BiDi/Input/IInputModule.cs index febb4479c5b97..5730b806133ee 100644 --- a/dotnet/src/webdriver/BiDi/Input/IInputModule.cs +++ b/dotnet/src/webdriver/BiDi/Input/IInputModule.cs @@ -1,3 +1,22 @@ +// +// 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 diff --git a/dotnet/src/webdriver/BiDi/Log/ILogModule.cs b/dotnet/src/webdriver/BiDi/Log/ILogModule.cs index d0f263f9dcd27..8accd921ccf56 100644 --- a/dotnet/src/webdriver/BiDi/Log/ILogModule.cs +++ b/dotnet/src/webdriver/BiDi/Log/ILogModule.cs @@ -1,3 +1,22 @@ +// +// 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 diff --git a/dotnet/src/webdriver/BiDi/Network/INetworkModule.cs b/dotnet/src/webdriver/BiDi/Network/INetworkModule.cs index 123a4e95ab9f6..cddfadeed3101 100644 --- a/dotnet/src/webdriver/BiDi/Network/INetworkModule.cs +++ b/dotnet/src/webdriver/BiDi/Network/INetworkModule.cs @@ -1,3 +1,22 @@ +// +// 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 diff --git a/dotnet/src/webdriver/BiDi/Permissions/IPermissionModule.cs b/dotnet/src/webdriver/BiDi/Permissions/IPermissionModule.cs deleted file mode 100644 index 4166d6abb3e1d..0000000000000 --- a/dotnet/src/webdriver/BiDi/Permissions/IPermissionModule.cs +++ /dev/null @@ -1,6 +0,0 @@ -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/IPermissionsModule.cs b/dotnet/src/webdriver/BiDi/Permissions/IPermissionsModule.cs new file mode 100644 index 0000000000000..6fada270754c2 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Permissions/IPermissionsModule.cs @@ -0,0 +1,25 @@ +// +// 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/Script/IScriptModule.cs b/dotnet/src/webdriver/BiDi/Script/IScriptModule.cs index 0399c12d8fe80..ff45ac79003db 100644 --- a/dotnet/src/webdriver/BiDi/Script/IScriptModule.cs +++ b/dotnet/src/webdriver/BiDi/Script/IScriptModule.cs @@ -1,3 +1,22 @@ +// +// 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; diff --git a/dotnet/src/webdriver/BiDi/Session/ISessionModule.cs b/dotnet/src/webdriver/BiDi/Session/ISessionModule.cs index 814942a96eff1..ec51ed7ccb55a 100644 --- a/dotnet/src/webdriver/BiDi/Session/ISessionModule.cs +++ b/dotnet/src/webdriver/BiDi/Session/ISessionModule.cs @@ -1,3 +1,22 @@ +// +// 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 diff --git a/dotnet/src/webdriver/BiDi/Speculation/ISpeculationModule.cs b/dotnet/src/webdriver/BiDi/Speculation/ISpeculationModule.cs index 29717b163ebee..2cf9c390c7472 100644 --- a/dotnet/src/webdriver/BiDi/Speculation/ISpeculationModule.cs +++ b/dotnet/src/webdriver/BiDi/Speculation/ISpeculationModule.cs @@ -1,3 +1,22 @@ +// +// 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 diff --git a/dotnet/src/webdriver/BiDi/Storage/IStorageModule.cs b/dotnet/src/webdriver/BiDi/Storage/IStorageModule.cs index 7cbf6f2af9470..251e75baadfaf 100644 --- a/dotnet/src/webdriver/BiDi/Storage/IStorageModule.cs +++ b/dotnet/src/webdriver/BiDi/Storage/IStorageModule.cs @@ -1,3 +1,22 @@ +// +// 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 diff --git a/dotnet/src/webdriver/BiDi/WebExtension/IWebExtensionModule.cs b/dotnet/src/webdriver/BiDi/WebExtension/IWebExtensionModule.cs index d6c3f909d1000..a3651ba6b90ce 100644 --- a/dotnet/src/webdriver/BiDi/WebExtension/IWebExtensionModule.cs +++ b/dotnet/src/webdriver/BiDi/WebExtension/IWebExtensionModule.cs @@ -1,3 +1,22 @@ +// +// 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 From 46912b3052165d506383e47811f6c464327199c6 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 19:34:52 +0300 Subject: [PATCH 09/25] Return BiDi.ConnectAsync() as public --- dotnet/src/webdriver/BiDi/BiDi.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/webdriver/BiDi/BiDi.cs b/dotnet/src/webdriver/BiDi/BiDi.cs index a7333655b0afe..82d8eb7cf868c 100644 --- a/dotnet/src/webdriver/BiDi/BiDi.cs +++ b/dotnet/src/webdriver/BiDi/BiDi.cs @@ -34,7 +34,7 @@ namespace OpenQA.Selenium.BiDi; -internal sealed class BiDi : IBiDi +public sealed class BiDi : IBiDi { private readonly ConcurrentDictionary _modules = new(); From dc9068de8fb810c9e2f22814e034060840b95f5b Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:03:16 +0300 Subject: [PATCH 10/25] Try to resolve circular deps --- dotnet/src/webdriver/BiDi/BiDi.cs | 12 ++++------ dotnet/src/webdriver/BiDi/Broker.cs | 24 +++++++------------- dotnet/src/webdriver/BiDi/EventDispatcher.cs | 17 ++++++-------- 3 files changed, 19 insertions(+), 34 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/BiDi.cs b/dotnet/src/webdriver/BiDi/BiDi.cs index 82d8eb7cf868c..1fe19aeb6265f 100644 --- a/dotnet/src/webdriver/BiDi/BiDi.cs +++ b/dotnet/src/webdriver/BiDi/BiDi.cs @@ -40,8 +40,6 @@ public sealed class BiDi : IBiDi private Broker Broker { get; set; } = null!; - private EventDispatcher EventDispatcher { get; set; } = null!; - internal ISessionModule Session => AsModule(); public IBrowsingContextModule BrowsingContext => AsModule(); @@ -64,14 +62,13 @@ public sealed class BiDi : IBiDi public static async Task ConnectAsync(string url, BiDiOptions? options = null, CancellationToken cancellationToken = default) { - BiDi bidi = new(); + var transport = new WebSocketTransport(new Uri(url)); - EventDispatcher eventDispatcher = new(bidi.Session, () => bidi); + await transport.ConnectAsync(cancellationToken).ConfigureAwait(false); - Broker broker = await Broker.CreateAsync(new Uri(url), eventDispatcher, cancellationToken).ConfigureAwait(false); + BiDi bidi = new(); - bidi.Broker = broker; - bidi.EventDispatcher = eventDispatcher; + bidi.Broker = new Broker(transport, bidi, () => bidi.Session); return bidi; } @@ -93,7 +90,6 @@ public Task EndAsync(EndOptions? options = null, CancellationToken cancellationT public async ValueTask DisposeAsync() { - await EventDispatcher.DisposeAsync().ConfigureAwait(false); await Broker.DisposeAsync().ConfigureAwait(false); GC.SuppressFinalize(this); } diff --git a/dotnet/src/webdriver/BiDi/Broker.cs b/dotnet/src/webdriver/BiDi/Broker.cs index 03a1fe1942bae..745925f47f4dd 100644 --- a/dotnet/src/webdriver/BiDi/Broker.cs +++ b/dotnet/src/webdriver/BiDi/Broker.cs @@ -20,6 +20,7 @@ using System.Collections.Concurrent; using System.Text.Json; using System.Text.Json.Serialization.Metadata; +using OpenQA.Selenium.BiDi.Session; using OpenQA.Selenium.Internal.Logging; namespace OpenQA.Selenium.BiDi; @@ -30,6 +31,7 @@ internal sealed class Broker : IAsyncDisposable private readonly ITransport _transport; private readonly EventDispatcher _eventDispatcher; + private readonly IBiDi _bidi; private readonly ConcurrentDictionary _pendingCommands = new(); @@ -40,34 +42,22 @@ internal sealed class Broker : IAsyncDisposable private readonly Task _receivingMessageTask; private readonly CancellationTokenSource _receiveMessagesCancellationTokenSource; - private Broker(ITransport transport, EventDispatcher eventDispatcher) + public Broker(ITransport transport, IBiDi bidi, Func sessionProvider) { _transport = transport; - _eventDispatcher = eventDispatcher; + _bidi = bidi; + _eventDispatcher = new EventDispatcher(sessionProvider); _receiveMessagesCancellationTokenSource = new CancellationTokenSource(); _receivingMessageTask = _myTaskFactory.StartNew(async () => await ReceiveMessagesAsync(_receiveMessagesCancellationTokenSource.Token), TaskCreationOptions.LongRunning).Unwrap(); } - public static async Task CreateAsync(Uri url, EventDispatcher eventDispatcher, CancellationToken cancellationToken) - { - var transport = new WebSocketTransport(url); - - await transport.ConnectAsync(cancellationToken).ConfigureAwait(false); - - return new Broker(transport, eventDispatcher); - } - public Task SubscribeAsync(string eventName, EventHandler eventHandler, SubscriptionOptions? options, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken) where TEventArgs : EventArgs { return _eventDispatcher.SubscribeAsync(eventName, eventHandler, options, jsonTypeInfo, cancellationToken); } - - - - public async Task ExecuteCommandAsync(TCommand command, CommandOptions? options, JsonTypeInfo jsonCommandTypeInfo, JsonTypeInfo jsonResultTypeInfo, CancellationToken cancellationToken) where TCommand : Command where TResult : EmptyResult @@ -98,6 +88,8 @@ public async ValueTask DisposeAsync() _receiveMessagesCancellationTokenSource.Cancel(); _receiveMessagesCancellationTokenSource.Dispose(); + await _eventDispatcher.DisposeAsync().ConfigureAwait(false); + await _receivingMessageTask.ConfigureAwait(false); _transport.Dispose(); @@ -197,7 +189,7 @@ private void ProcessReceivedMessage(byte[]? data) { var eventArgs = (EventArgs)JsonSerializer.Deserialize(ref paramsReader, eventInfo)!; - _eventDispatcher.EnqueueEvent(method, eventArgs); + _eventDispatcher.EnqueueEvent(method, eventArgs, _bidi); } else { diff --git a/dotnet/src/webdriver/BiDi/EventDispatcher.cs b/dotnet/src/webdriver/BiDi/EventDispatcher.cs index 77446d9c01f33..5b83b9f049e1c 100644 --- a/dotnet/src/webdriver/BiDi/EventDispatcher.cs +++ b/dotnet/src/webdriver/BiDi/EventDispatcher.cs @@ -29,8 +29,7 @@ internal sealed class EventDispatcher : IAsyncDisposable { private readonly ILogger _logger = Internal.Logging.Log.GetLogger(); - private readonly ISessionModule _session; - private readonly Func _bidiProvider; + private readonly Func _sessionProvider; private readonly ConcurrentDictionary> _eventHandlers = new(); private readonly Dictionary _eventTypesMap = []; @@ -45,11 +44,9 @@ internal sealed class EventDispatcher : IAsyncDisposable private static readonly TaskFactory _myTaskFactory = new(CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskContinuationOptions.None, TaskScheduler.Default); - public EventDispatcher(ISessionModule session, Func bidiProvider) + public EventDispatcher(Func sessionProvider) { - _session = session; - _bidiProvider = bidiProvider; - + _sessionProvider = sessionProvider; _eventEmitterTask = _myTaskFactory.StartNew(ProcessEventsAwaiterAsync).Unwrap(); } @@ -60,7 +57,7 @@ public async Task SubscribeAsync(string eventName, Eve var handlers = _eventHandlers.GetOrAdd(eventName, (a) => []); - var subscribeResult = await _session.SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken).ConfigureAwait(false); + var subscribeResult = await _sessionProvider().SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken).ConfigureAwait(false); handlers.Add(eventHandler); @@ -73,12 +70,12 @@ public async Task UnsubscribeAsync(Subscription subscription, CancellationToken eventHandlers.Remove(subscription.EventHandler); - await _session.UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false); + await _sessionProvider().UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false); } - internal void EnqueueEvent(string method, EventArgs eventArgs) + internal void EnqueueEvent(string method, EventArgs eventArgs, IBiDi bidi) { - eventArgs.BiDi = _bidiProvider(); + eventArgs.BiDi = bidi; _pendingEvents.Writer.TryWrite(new EventInfo(method, eventArgs)); } From 64222d0d0358bb72117acca1c3d1a306546f37b7 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:05:36 +0300 Subject: [PATCH 11/25] Session end result --- dotnet/src/webdriver/BiDi/BiDi.cs | 2 +- dotnet/src/webdriver/BiDi/IBiDi.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/BiDi.cs b/dotnet/src/webdriver/BiDi/BiDi.cs index 1fe19aeb6265f..4b1335dba4fac 100644 --- a/dotnet/src/webdriver/BiDi/BiDi.cs +++ b/dotnet/src/webdriver/BiDi/BiDi.cs @@ -83,7 +83,7 @@ public Task NewAsync(CapabilitiesRequest capabilities, NewOptions? op return Session.NewAsync(capabilities, options, cancellationToken); } - public Task EndAsync(EndOptions? options = null, CancellationToken cancellationToken = default) + public Task EndAsync(EndOptions? options = null, CancellationToken cancellationToken = default) { return Session.EndAsync(options, cancellationToken); } diff --git a/dotnet/src/webdriver/BiDi/IBiDi.cs b/dotnet/src/webdriver/BiDi/IBiDi.cs index 2f58204055c2a..2410351fb7fa3 100644 --- a/dotnet/src/webdriver/BiDi/IBiDi.cs +++ b/dotnet/src/webdriver/BiDi/IBiDi.cs @@ -55,7 +55,7 @@ public interface IBiDi : IAsyncDisposable Task NewAsync(CapabilitiesRequest capabilities, NewOptions? options = null, CancellationToken cancellationToken = default); - Task EndAsync(EndOptions? options = null, CancellationToken cancellationToken = default); + Task EndAsync(EndOptions? options = null, CancellationToken cancellationToken = default); T AsModule() where T : Module, new(); } From be4c11f4a5a955cec581958436116d9b442cfc4e Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:38:35 +0300 Subject: [PATCH 12/25] Unsubscribe twice issue, delegate event deserialization --- dotnet/src/webdriver/BiDi/Broker.cs | 13 +------- dotnet/src/webdriver/BiDi/EventDispatcher.cs | 32 ++++++++++++++------ dotnet/src/webdriver/BiDi/Subscription.cs | 1 + 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/Broker.cs b/dotnet/src/webdriver/BiDi/Broker.cs index 745925f47f4dd..66cac48e5e87e 100644 --- a/dotnet/src/webdriver/BiDi/Broker.cs +++ b/dotnet/src/webdriver/BiDi/Broker.cs @@ -184,18 +184,7 @@ private void ProcessReceivedMessage(byte[]? data) case "event": if (method is null) throw new JsonException("The remote end responded with 'event' message type, but missed required 'method' property."); - - if (_eventDispatcher.TryGetEventTypeInfo(method, out var eventInfo) && eventInfo is not null) - { - var eventArgs = (EventArgs)JsonSerializer.Deserialize(ref paramsReader, eventInfo)!; - - _eventDispatcher.EnqueueEvent(method, eventArgs, _bidi); - } - else - { - throw new BiDiException($"The remote end responded with 'event' message type, but no event type mapping for method '{method}' was found."); - } - + _eventDispatcher.EnqueueEvent(method, ref paramsReader, _bidi); break; case "error": diff --git a/dotnet/src/webdriver/BiDi/EventDispatcher.cs b/dotnet/src/webdriver/BiDi/EventDispatcher.cs index 5b83b9f049e1c..d8adb7064dd82 100644 --- a/dotnet/src/webdriver/BiDi/EventDispatcher.cs +++ b/dotnet/src/webdriver/BiDi/EventDispatcher.cs @@ -18,6 +18,7 @@ // using System.Collections.Concurrent; +using System.Text.Json; using System.Text.Json.Serialization.Metadata; using System.Threading.Channels; using OpenQA.Selenium.BiDi.Session; @@ -66,23 +67,36 @@ public async Task SubscribeAsync(string eventName, Eve public async Task UnsubscribeAsync(Subscription subscription, CancellationToken cancellationToken) { - var eventHandlers = _eventHandlers[subscription.EventHandler.EventName]; - - eventHandlers.Remove(subscription.EventHandler); + if (_eventHandlers.TryGetValue(subscription.EventHandler.EventName, out var eventHandlers)) + { + eventHandlers.Remove(subscription.EventHandler); + } await _sessionProvider().UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false); } - internal void EnqueueEvent(string method, EventArgs eventArgs, IBiDi bidi) + internal bool TryGetEventTypeInfo(string method, out JsonTypeInfo? jsonTypeInfo) { - eventArgs.BiDi = bidi; - - _pendingEvents.Writer.TryWrite(new EventInfo(method, eventArgs)); + return _eventTypesMap.TryGetValue(method, out jsonTypeInfo); } - internal bool TryGetEventTypeInfo(string method, out JsonTypeInfo? jsonTypeInfo) + public void EnqueueEvent(string method, ref Utf8JsonReader paramsReader, IBiDi bidi) { - return _eventTypesMap.TryGetValue(method, out jsonTypeInfo); + if (_eventTypesMap.TryGetValue(method, out var eventInfo) && eventInfo is not null) + { + var eventArgs = (EventArgs)JsonSerializer.Deserialize(ref paramsReader, eventInfo)!; + + eventArgs.BiDi = bidi; + + _pendingEvents.Writer.TryWrite(new EventInfo(method, eventArgs)); + } + 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() diff --git a/dotnet/src/webdriver/BiDi/Subscription.cs b/dotnet/src/webdriver/BiDi/Subscription.cs index 4e31e6bb5a3a9..f1ac5580ec34c 100644 --- a/dotnet/src/webdriver/BiDi/Subscription.cs +++ b/dotnet/src/webdriver/BiDi/Subscription.cs @@ -42,6 +42,7 @@ public async Task UnsubscribeAsync(CancellationToken cancellationToken = default public async ValueTask DisposeAsync() { await UnsubscribeAsync().ConfigureAwait(false); + GC.SuppressFinalize(this); } } From 915eab44ae08d2eebc35b5953a9a367a08143443 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:40:24 +0300 Subject: [PATCH 13/25] It is not JsonException, it is validation errors --- dotnet/src/webdriver/BiDi/Broker.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/Broker.cs b/dotnet/src/webdriver/BiDi/Broker.cs index 66cac48e5e87e..c07fc5b4eaa0b 100644 --- a/dotnet/src/webdriver/BiDi/Broker.cs +++ b/dotnet/src/webdriver/BiDi/Broker.cs @@ -155,14 +155,14 @@ private void ProcessReceivedMessage(byte[]? data) 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); } @@ -183,12 +183,12 @@ 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 (method is null) throw new BiDiException("The remote end responded with 'event' message type, but missed required 'method' property."); _eventDispatcher.EnqueueEvent(method, ref paramsReader, _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)) { From 3eb4844c7082038f6b40faab6ea5dabf20388959 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:44:41 +0300 Subject: [PATCH 14/25] Avoid massive usings --- dotnet/src/webdriver/BiDi/BiDi.cs | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/BiDi.cs b/dotnet/src/webdriver/BiDi/BiDi.cs index 4b1335dba4fac..4a57a8ed25c0f 100644 --- a/dotnet/src/webdriver/BiDi/BiDi.cs +++ b/dotnet/src/webdriver/BiDi/BiDi.cs @@ -20,17 +20,7 @@ using System.Collections.Concurrent; using System.Text.Json; using System.Text.Json.Serialization; -using OpenQA.Selenium.BiDi.Browser; -using OpenQA.Selenium.BiDi.BrowsingContext; -using OpenQA.Selenium.BiDi.Emulation; -using OpenQA.Selenium.BiDi.Input; -using OpenQA.Selenium.BiDi.Json.Converters; -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; @@ -42,23 +32,23 @@ public sealed class BiDi : IBiDi internal ISessionModule Session => AsModule(); - public IBrowsingContextModule BrowsingContext => AsModule(); + public BrowsingContext.IBrowsingContextModule BrowsingContext => AsModule(); - public IBrowserModule Browser => AsModule(); + public Browser.IBrowserModule Browser => AsModule(); - public INetworkModule Network => AsModule(); + public Network.INetworkModule Network => AsModule(); - public IInputModule Input => AsModule(); + public Input.IInputModule Input => AsModule(); - public IScriptModule Script => AsModule(); + public Script.IScriptModule Script => AsModule(); - public ILogModule Log => AsModule(); + public Log.ILogModule Log => AsModule(); - public IStorageModule Storage => AsModule(); + public Storage.IStorageModule Storage => AsModule(); - public IWebExtensionModule WebExtension => AsModule(); + public WebExtension.IWebExtensionModule WebExtension => AsModule(); - public IEmulationModule Emulation => AsModule(); + public Emulation.IEmulationModule Emulation => AsModule(); public static async Task ConnectAsync(string url, BiDiOptions? options = null, CancellationToken cancellationToken = default) { @@ -108,7 +98,7 @@ private static JsonSerializerOptions CreateDefaultJsonOptions() DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Converters = { - new DateTimeOffsetConverter(), + new Json.Converters.DateTimeOffsetConverter(), } }; } From a0a6cbd3ee9602007d7ba29b56e21a6e4fe8e7a8 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:50:39 +0300 Subject: [PATCH 15/25] Remove unnecessary TryGetEventTypeInfo --- dotnet/src/webdriver/BiDi/EventDispatcher.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/EventDispatcher.cs b/dotnet/src/webdriver/BiDi/EventDispatcher.cs index d8adb7064dd82..06adf9b8b1809 100644 --- a/dotnet/src/webdriver/BiDi/EventDispatcher.cs +++ b/dotnet/src/webdriver/BiDi/EventDispatcher.cs @@ -75,11 +75,6 @@ public async Task UnsubscribeAsync(Subscription subscription, CancellationToken await _sessionProvider().UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false); } - internal bool TryGetEventTypeInfo(string method, out JsonTypeInfo? jsonTypeInfo) - { - return _eventTypesMap.TryGetValue(method, out jsonTypeInfo); - } - public void EnqueueEvent(string method, ref Utf8JsonReader paramsReader, IBiDi bidi) { if (_eventTypesMap.TryGetValue(method, out var eventInfo) && eventInfo is not null) From 5d32fecc699885360bb4c3ca62f4434c393dd4f9 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:55:10 +0300 Subject: [PATCH 16/25] Unsubscribe --- dotnet/src/webdriver/BiDi/EventDispatcher.cs | 5 ++--- dotnet/src/webdriver/BiDi/Subscription.cs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/EventDispatcher.cs b/dotnet/src/webdriver/BiDi/EventDispatcher.cs index 06adf9b8b1809..e24b0f08ace2d 100644 --- a/dotnet/src/webdriver/BiDi/EventDispatcher.cs +++ b/dotnet/src/webdriver/BiDi/EventDispatcher.cs @@ -65,14 +65,13 @@ public async Task SubscribeAsync(string eventName, Eve return new Subscription(subscribeResult.Subscription, this, eventHandler); } - public async Task UnsubscribeAsync(Subscription subscription, CancellationToken cancellationToken) + public async ValueTask UnsubscribeAsync(Subscription subscription, CancellationToken cancellationToken) { if (_eventHandlers.TryGetValue(subscription.EventHandler.EventName, out var eventHandlers)) { + await _sessionProvider().UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false); eventHandlers.Remove(subscription.EventHandler); } - - await _sessionProvider().UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false); } public void EnqueueEvent(string method, ref Utf8JsonReader paramsReader, IBiDi bidi) diff --git a/dotnet/src/webdriver/BiDi/Subscription.cs b/dotnet/src/webdriver/BiDi/Subscription.cs index f1ac5580ec34c..319e346b9cdc0 100644 --- a/dotnet/src/webdriver/BiDi/Subscription.cs +++ b/dotnet/src/webdriver/BiDi/Subscription.cs @@ -34,7 +34,7 @@ internal Subscription(Session.Subscription subscription, EventDispatcher eventDi internal EventHandler EventHandler { get; } - public async Task UnsubscribeAsync(CancellationToken cancellationToken = default) + public async ValueTask UnsubscribeAsync(CancellationToken cancellationToken = default) { await _eventDispatcher.UnsubscribeAsync(this, cancellationToken).ConfigureAwait(false); } From 87170d290a7c2410db8e68df21893027b55d3f1a Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:18:00 +0300 Subject: [PATCH 17/25] Deferred events deserialization --- dotnet/src/webdriver/BiDi/Broker.cs | 16 ++++++- dotnet/src/webdriver/BiDi/EventDispatcher.cs | 48 ++++++++++---------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/Broker.cs b/dotnet/src/webdriver/BiDi/Broker.cs index c07fc5b4eaa0b..86230da1a8f3f 100644 --- a/dotnet/src/webdriver/BiDi/Broker.cs +++ b/dotnet/src/webdriver/BiDi/Broker.cs @@ -106,6 +106,8 @@ private void ProcessReceivedMessage(byte[]? data) string? message = default; Utf8JsonReader resultReader = default; Utf8JsonReader paramsReader = default; + long paramsStartIndex = 0; + long paramsEndIndex = 0; Utf8JsonReader reader = new(new ReadOnlySpan(data)); reader.Read(); @@ -137,6 +139,7 @@ private void ProcessReceivedMessage(byte[]? data) case "params": paramsReader = reader; // snapshot + paramsStartIndex = reader.TokenStartIndex; break; case "error": @@ -148,7 +151,15 @@ private void ProcessReceivedMessage(byte[]? data) break; } - reader.Skip(); + if (propertyName == "params") + { + reader.Skip(); + paramsEndIndex = reader.BytesConsumed; + } + else + { + reader.Skip(); + } reader.Read(); } @@ -184,7 +195,8 @@ private void ProcessReceivedMessage(byte[]? data) case "event": if (method is null) throw new BiDiException("The remote end responded with 'event' message type, but missed required 'method' property."); - _eventDispatcher.EnqueueEvent(method, ref paramsReader, _bidi); + var paramsJsonData = new ReadOnlyMemory(data, (int)paramsStartIndex, (int)(paramsEndIndex - paramsStartIndex)); + _eventDispatcher.EnqueueEvent(method, paramsJsonData, _bidi); break; case "error": diff --git a/dotnet/src/webdriver/BiDi/EventDispatcher.cs b/dotnet/src/webdriver/BiDi/EventDispatcher.cs index e24b0f08ace2d..1a4e93006821c 100644 --- a/dotnet/src/webdriver/BiDi/EventDispatcher.cs +++ b/dotnet/src/webdriver/BiDi/EventDispatcher.cs @@ -32,10 +32,9 @@ internal sealed class EventDispatcher : IAsyncDisposable private readonly Func _sessionProvider; - private readonly ConcurrentDictionary> _eventHandlers = new(); - private readonly Dictionary _eventTypesMap = []; + private readonly ConcurrentDictionary _events = new(); - private readonly Channel _pendingEvents = Channel.CreateUnbounded(new() + private readonly Channel _pendingEvents = Channel.CreateUnbounded(new() { SingleReader = true, SingleWriter = true @@ -54,35 +53,29 @@ public EventDispatcher(Func sessionProvider) 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 registration = _events.GetOrAdd(eventName, _ => new EventRegistration(jsonTypeInfo)); var subscribeResult = await _sessionProvider().SubscribeAsync([eventName], new() { Contexts = options?.Contexts, UserContexts = options?.UserContexts }, cancellationToken).ConfigureAwait(false); - handlers.Add(eventHandler); + registration.Handlers.Add(eventHandler); return new Subscription(subscribeResult.Subscription, this, eventHandler); } public async ValueTask UnsubscribeAsync(Subscription subscription, CancellationToken cancellationToken) { - if (_eventHandlers.TryGetValue(subscription.EventHandler.EventName, out var eventHandlers)) + if (_events.TryGetValue(subscription.EventHandler.EventName, out var registration)) { await _sessionProvider().UnsubscribeAsync([subscription.SubscriptionId], null, cancellationToken).ConfigureAwait(false); - eventHandlers.Remove(subscription.EventHandler); + registration.Handlers.Remove(subscription.EventHandler); } } - public void EnqueueEvent(string method, ref Utf8JsonReader paramsReader, IBiDi bidi) + public void EnqueueEvent(string method, ReadOnlyMemory jsonUtf8Bytes, IBiDi bidi) { - if (_eventTypesMap.TryGetValue(method, out var eventInfo) && eventInfo is not null) + if (_events.TryGetValue(method, out var registration) && registration.TypeInfo is not null) { - var eventArgs = (EventArgs)JsonSerializer.Deserialize(ref paramsReader, eventInfo)!; - - eventArgs.BiDi = bidi; - - _pendingEvents.Writer.TryWrite(new EventInfo(method, eventArgs)); + _pendingEvents.Writer.TryWrite(new PendingEvent(method, jsonUtf8Bytes, bidi, registration.TypeInfo)); } else { @@ -102,16 +95,15 @@ private async Task ProcessEventsAwaiterAsync() { try { - if (_eventHandlers.TryGetValue(result.Method, out var eventHandlers)) + if (_events.TryGetValue(result.Method, out var registration)) { - if (eventHandlers is not null) - { - foreach (var handler in eventHandlers.ToArray()) // copy handlers avoiding modified collection while iterating - { - var args = result.Params; + // Deserialize on background thread instead of network thread (single parse) + var eventArgs = (EventArgs)JsonSerializer.Deserialize(result.JsonUtf8Bytes.Span, result.TypeInfo)!; + eventArgs.BiDi = result.BiDi; - await handler.InvokeAsync(args).ConfigureAwait(false); - } + foreach (var handler in registration.Handlers.ToArray()) // copy handlers avoiding modified collection while iterating + { + await handler.InvokeAsync(eventArgs).ConfigureAwait(false); } } } @@ -135,5 +127,11 @@ public async ValueTask DisposeAsync() GC.SuppressFinalize(this); } - private readonly record struct EventInfo(string Method, EventArgs Params); + 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; } = []; + } } From 4524428a3f8b02f5e8a3e49e0caef9442bb3448f Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:23:40 +0300 Subject: [PATCH 18/25] Ignore OperationCanceledException on dispose --- dotnet/src/webdriver/BiDi/Broker.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dotnet/src/webdriver/BiDi/Broker.cs b/dotnet/src/webdriver/BiDi/Broker.cs index 86230da1a8f3f..58d24b53f36d9 100644 --- a/dotnet/src/webdriver/BiDi/Broker.cs +++ b/dotnet/src/webdriver/BiDi/Broker.cs @@ -90,7 +90,14 @@ public async ValueTask DisposeAsync() await _eventDispatcher.DisposeAsync().ConfigureAwait(false); - await _receivingMessageTask.ConfigureAwait(false); + try + { + await _receivingMessageTask.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Expected when cancellation is requested, ignore. + } _transport.Dispose(); From 97251c7b8dddb4f1b01b96dccda4e7b6e27f13f9 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:26:45 +0300 Subject: [PATCH 19/25] Only when expected --- dotnet/src/webdriver/BiDi/Broker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/webdriver/BiDi/Broker.cs b/dotnet/src/webdriver/BiDi/Broker.cs index 58d24b53f36d9..4d871967bcfbe 100644 --- a/dotnet/src/webdriver/BiDi/Broker.cs +++ b/dotnet/src/webdriver/BiDi/Broker.cs @@ -94,7 +94,7 @@ public async ValueTask DisposeAsync() { await _receivingMessageTask.ConfigureAwait(false); } - catch (OperationCanceledException) + catch (OperationCanceledException) when (_receiveMessagesCancellationTokenSource.IsCancellationRequested) { // Expected when cancellation is requested, ignore. } From 009163689b30d5d46315434e9d2c232f41ca70ed Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:38:15 +0300 Subject: [PATCH 20/25] BiDi private ctor --- dotnet/src/webdriver/BiDi/BiDi.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dotnet/src/webdriver/BiDi/BiDi.cs b/dotnet/src/webdriver/BiDi/BiDi.cs index 4a57a8ed25c0f..4fdfe372a9260 100644 --- a/dotnet/src/webdriver/BiDi/BiDi.cs +++ b/dotnet/src/webdriver/BiDi/BiDi.cs @@ -32,6 +32,8 @@ public sealed class BiDi : IBiDi internal ISessionModule Session => AsModule(); + private BiDi() { } + public BrowsingContext.IBrowsingContextModule BrowsingContext => AsModule(); public Browser.IBrowserModule Browser => AsModule(); From 07901f439afa201d307c87724b212e3d997f7cff Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:53:17 +0300 Subject: [PATCH 21/25] Cancel/await/dispose token source --- dotnet/src/webdriver/BiDi/Broker.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dotnet/src/webdriver/BiDi/Broker.cs b/dotnet/src/webdriver/BiDi/Broker.cs index 4d871967bcfbe..2448e348d7a7a 100644 --- a/dotnet/src/webdriver/BiDi/Broker.cs +++ b/dotnet/src/webdriver/BiDi/Broker.cs @@ -86,7 +86,6 @@ public async Task ExecuteCommandAsync(TCommand comma public async ValueTask DisposeAsync() { _receiveMessagesCancellationTokenSource.Cancel(); - _receiveMessagesCancellationTokenSource.Dispose(); await _eventDispatcher.DisposeAsync().ConfigureAwait(false); @@ -99,6 +98,8 @@ public async ValueTask DisposeAsync() // Expected when cancellation is requested, ignore. } + _receiveMessagesCancellationTokenSource.Dispose(); + _transport.Dispose(); GC.SuppressFinalize(this); From 8c9a519a5a2ecf79f6862e35b39b3ca16d5a9c5c Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:12:28 +0300 Subject: [PATCH 22/25] Clean paramsReader --- dotnet/src/webdriver/BiDi/Broker.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/Broker.cs b/dotnet/src/webdriver/BiDi/Broker.cs index 2448e348d7a7a..5f5477f010276 100644 --- a/dotnet/src/webdriver/BiDi/Broker.cs +++ b/dotnet/src/webdriver/BiDi/Broker.cs @@ -113,7 +113,6 @@ private void ProcessReceivedMessage(byte[]? data) string? error = default; string? message = default; Utf8JsonReader resultReader = default; - Utf8JsonReader paramsReader = default; long paramsStartIndex = 0; long paramsEndIndex = 0; @@ -146,7 +145,6 @@ private void ProcessReceivedMessage(byte[]? data) break; case "params": - paramsReader = reader; // snapshot paramsStartIndex = reader.TokenStartIndex; break; From b5b59357f8970ff2447be01a5da2146426044ff1 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:29:11 +0300 Subject: [PATCH 23/25] Dispose in ctor --- dotnet/src/webdriver/BiDi/BiDi.cs | 4 +--- dotnet/src/webdriver/BiDi/ITransport.cs | 2 -- .../src/webdriver/BiDi/WebSocketTransport.cs | 22 +++++++++++++++---- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/BiDi.cs b/dotnet/src/webdriver/BiDi/BiDi.cs index 4fdfe372a9260..b65b15078cbd1 100644 --- a/dotnet/src/webdriver/BiDi/BiDi.cs +++ b/dotnet/src/webdriver/BiDi/BiDi.cs @@ -54,9 +54,7 @@ private BiDi() { } public static async Task ConnectAsync(string url, BiDiOptions? options = null, CancellationToken cancellationToken = default) { - var transport = new WebSocketTransport(new Uri(url)); - - await transport.ConnectAsync(cancellationToken).ConfigureAwait(false); + var transport = await WebSocketTransport.ConnectAsync(new Uri(url), cancellationToken).ConfigureAwait(false); BiDi bidi = 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/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) From 6f844c7bb809531de93b20df890114aed6880a19 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:16:01 +0300 Subject: [PATCH 24/25] Hide AsModule IDE --- dotnet/src/webdriver/BiDi/IBiDi.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dotnet/src/webdriver/BiDi/IBiDi.cs b/dotnet/src/webdriver/BiDi/IBiDi.cs index 2410351fb7fa3..64b06332659a9 100644 --- a/dotnet/src/webdriver/BiDi/IBiDi.cs +++ b/dotnet/src/webdriver/BiDi/IBiDi.cs @@ -18,6 +18,7 @@ // under the License. // +using System.ComponentModel; using OpenQA.Selenium.BiDi.Browser; using OpenQA.Selenium.BiDi.BrowsingContext; using OpenQA.Selenium.BiDi.Emulation; @@ -57,5 +58,6 @@ public interface IBiDi : IAsyncDisposable Task EndAsync(EndOptions? options = null, CancellationToken cancellationToken = default); + [EditorBrowsable(EditorBrowsableState.Never)] T AsModule() where T : Module, new(); } From a47cc97aba78164bc28d0f7ff1103ff4f010b69a Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:01:24 +0300 Subject: [PATCH 25/25] Format --- dotnet/src/webdriver/BiDi/IBiDi.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/webdriver/BiDi/IBiDi.cs b/dotnet/src/webdriver/BiDi/IBiDi.cs index 64b06332659a9..c7f5d13765e78 100644 --- a/dotnet/src/webdriver/BiDi/IBiDi.cs +++ b/dotnet/src/webdriver/BiDi/IBiDi.cs @@ -1,4 +1,3 @@ - // // Licensed to the Software Freedom Conservancy (SFC) under one // or more contributor license agreements. See the NOTICE file