Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7b398a1
Remove stateless converters from json options
nvborisenko Dec 3, 2025
b0f5e1e
Generated options
nvborisenko Dec 3, 2025
79f723d
Hidrate the BiDi property example
nvborisenko Dec 3, 2025
cafbba5
One more hidrate example
nvborisenko Dec 3, 2025
d7956b4
Unified hidration for top level types
nvborisenko Dec 3, 2025
aaddb1e
Fix name
nvborisenko Dec 3, 2025
25389bd
Hydrate event args
nvborisenko Dec 3, 2025
61ad320
Hydrate AddInterceptResult
nvborisenko Dec 3, 2025
2d20a43
Hydrate AddDataCollectorResult
nvborisenko Dec 3, 2025
3f482c0
Hydrate CreateResult
nvborisenko Dec 3, 2025
00d499e
Hydrate AddPreloadScriptResult
nvborisenko Dec 3, 2025
7204604
Hydrate InstallResult
nvborisenko Dec 3, 2025
a465945
Throw if not hydrated
nvborisenko Dec 3, 2025
4e487df
Fix property name for exception
nvborisenko Dec 3, 2025
1501af1
Don't hydrate primitives except BrowsingContext
nvborisenko Dec 3, 2025
d1a2fcb
High level interception is not hydratable
nvborisenko Dec 3, 2025
372fac0
Use shared json options
nvborisenko Dec 6, 2025
3aca801
Remove generated context options
nvborisenko Dec 6, 2025
125f52e
Remove hydration
nvborisenko Dec 6, 2025
713ad83
Returned back some primitives
nvborisenko Dec 6, 2025
5861966
Some oothers
nvborisenko Dec 6, 2025
542f49f
All primitive types are now hydratable
nvborisenko Dec 6, 2025
f3ed0a4
Allow public ctor for hydratable primitives
nvborisenko Dec 6, 2025
9ab3b93
Add note for converters
nvborisenko Dec 6, 2025
cb902a5
Moved WebExtensionConverter
nvborisenko Dec 6, 2025
d214de3
Moved CollectorConverter and InterceptConverter
nvborisenko Dec 6, 2025
3bd611a
Moved PreloadScriptConverter
nvborisenko Dec 6, 2025
b4354d5
Moved RealmConverter
nvborisenko Dec 6, 2025
66aa3ff
Update LogModule.cs
nvborisenko Dec 6, 2025
8235211
Moved InternalIdConverter
nvborisenko Dec 6, 2025
f3e65eb
Moved HandleConverter
nvborisenko Dec 6, 2025
1ddd37d
Moved BrowserUserContextConverter
nvborisenko Dec 6, 2025
015d863
All converters as module requires
nvborisenko Dec 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 17 additions & 30 deletions dotnet/src/webdriver/BiDi/BiDi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,29 @@ public sealed class BiDi : IAsyncDisposable
{
private readonly ConcurrentDictionary<Type, Module> _modules = new();

private readonly JsonSerializerOptions _jsonOptions;

private BiDi(string url)
{
var uri = new Uri(url);

Broker = new Broker(this, uri);

_jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,

Converters =
{
new DateTimeOffsetConverter(),
}
};
}

private Broker Broker { get; }

internal Session.SessionModule SessionModule => AsModule<Session.SessionModule>();

public BrowsingContext.BrowsingContextModule BrowsingContext => AsModule<BrowsingContext.BrowsingContextModule>();
Expand Down Expand Up @@ -85,35 +101,6 @@ public async ValueTask DisposeAsync()

public T AsModule<T>() where T : Module, new()
{
return (T)_modules.GetOrAdd(typeof(T), _ => Module.Create<T>(this, Broker, GetJsonOptions()));
}

private Broker Broker { get; }

private JsonSerializerOptions GetJsonOptions()
{
return new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,

// BiDi returns special numbers such as "NaN" as strings
// Additionally, -0 is returned as a string "-0"
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals | JsonNumberHandling.AllowReadingFromString,
Converters =
{
new BrowsingContextConverter(this),
new BrowserUserContextConverter(this),
new CollectorConverter(this),
new InterceptConverter(this),
new HandleConverter(this),
new InternalIdConverter(this),
new PreloadScriptConverter(this),
new RealmConverter(this),
new DateTimeOffsetConverter(),
new WebExtensionConverter(this),
}
};
return (T)_modules.GetOrAdd(typeof(T), _ => Module.Create<T>(this, Broker, _jsonOptions));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this still support adding a module after BiDi is created?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, each module creates new copy of provided instance.

var logOptions = new JsonSerializerOptions(jsonSerializerOptions)
{
    Converters =
    {
        new BrowsingContextConverter(BiDi),
        new RealmConverter(BiDi),
        new InternalIdConverter(BiDi),
        new HandleConverter(BiDi),
    }
};

_jsonContext = new LogJsonSerializerContext(logOptions);

We can simplify it, and always put new instance into module Initialize(...). I would be better for external modules. Will do.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
}
2 changes: 2 additions & 0 deletions dotnet/src/webdriver/BiDi/Broker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ private void ProcessReceivedMessage(byte[]? data)
{
var eventArgs = (EventArgs)JsonSerializer.Deserialize(ref paramsReader, eventInfo)!;

eventArgs.BiDi = _bidi;

var messageEvent = (method, eventArgs);
_pendingEvents.Add(messageEvent);
}
Expand Down
14 changes: 12 additions & 2 deletions dotnet/src/webdriver/BiDi/Browser/BrowserModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
// under the License.
// </copyright>

using OpenQA.Selenium.BiDi.Json.Converters;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
Expand Down Expand Up @@ -78,9 +79,17 @@ public async Task<SetDownloadBehaviorResult> SetDownloadBehaviorDeniedAsync(SetD
return await Broker.ExecuteCommandAsync(new SetDownloadBehaviorCommand(@params), options, _jsonContext.SetDownloadBehaviorCommand, _jsonContext.SetDownloadBehaviorResult).ConfigureAwait(false);
}

protected override void Initialize(JsonSerializerOptions options)
protected override void Initialize(JsonSerializerOptions jsonSerializerOptions)
{
_jsonContext = new BrowserJsonSerializerContext(options);
var browserOptions = new JsonSerializerOptions(jsonSerializerOptions)
{
Converters =
{
new BrowserUserContextConverter(BiDi),
}
};

_jsonContext = new BrowserJsonSerializerContext(browserOptions);
}
}

Expand All @@ -96,4 +105,5 @@ protected override void Initialize(JsonSerializerOptions options)
[JsonSerializable(typeof(GetClientWindowsResult))]
[JsonSerializable(typeof(SetDownloadBehaviorCommand))]
[JsonSerializable(typeof(SetDownloadBehaviorResult))]

internal partial class BrowserJsonSerializerContext : JsonSerializerContext;
41 changes: 14 additions & 27 deletions dotnet/src/webdriver/BiDi/Browser/UserContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,45 +18,32 @@
// </copyright>

using System;
using System.Threading.Tasks;
using System.Text.Json.Serialization;

namespace OpenQA.Selenium.BiDi.Browser;

public sealed class UserContext : IEquatable<UserContext>, IAsyncDisposable
public sealed record UserContext
{
private readonly BiDi _bidi;

internal UserContext(BiDi bidi, string id)
{
_bidi = bidi;
Id = id;
}

internal string Id { get; }

public Task RemoveAsync()
public UserContext(BiDi bidi, string id)
: this(id)
{
return _bidi.Browser.RemoveUserContextAsync(this);
BiDi = bidi ?? throw new ArgumentNullException(nameof(bidi));
}

public async ValueTask DisposeAsync()
[JsonConstructor]
internal UserContext(string id)
{
await RemoveAsync().ConfigureAwait(false);
}

public bool Equals(UserContext? other)
{
return other is not null && string.Equals(Id, other.Id, StringComparison.Ordinal);
Id = id;
}

internal string Id { get; }

public override bool Equals(object? obj)
{
return Equals(obj as UserContext);
}
private BiDi? _bidi;

public override int GetHashCode()
[JsonIgnore]
public BiDi BiDi
{
return StringComparer.Ordinal.GetHashCode(Id);
get => _bidi ?? throw new InvalidOperationException($"{nameof(BiDi)} instance has not been hydrated.");
internal set => _bidi = value;
}
}
18 changes: 15 additions & 3 deletions dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,15 @@ namespace OpenQA.Selenium.BiDi.BrowsingContext;

public sealed class BrowsingContext
{
internal BrowsingContext(BiDi bidi, string id)
public BrowsingContext(BiDi bidi, string id)
: this(id)
{
BiDi = bidi ?? throw new ArgumentNullException(nameof(bidi));
}

[JsonConstructor]
internal BrowsingContext(string id)
{
BiDi = bidi;
Id = id;
}

Expand All @@ -40,8 +46,14 @@ internal BrowsingContext(BiDi bidi, string id)

internal string Id { get; }

private BiDi? _bidi;

[JsonIgnore]
public BiDi BiDi { get; }
public BiDi BiDi
{
get => _bidi ?? throw new InvalidOperationException($"{nameof(BiDi)} instance has not been hydrated.");
internal set => _bidi = value;
}

[JsonIgnore]
public BrowsingContextLogModule Log => _logModule ?? Interlocked.CompareExchange(ref _logModule, new BrowsingContextLogModule(this, BiDi.Log), null) ?? _logModule;
Expand Down
17 changes: 15 additions & 2 deletions dotnet/src/webdriver/BiDi/BrowsingContext/BrowsingContextModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
// under the License.
// </copyright>

using OpenQA.Selenium.BiDi.Json.Converters;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
Expand Down Expand Up @@ -252,9 +253,20 @@ public async Task<Subscription> OnUserPromptClosedAsync(Action<UserPromptClosedE
return await Broker.SubscribeAsync("browsingContext.userPromptClosed", handler, options, _jsonContext.UserPromptClosedEventArgs).ConfigureAwait(false);
}

protected override void Initialize(JsonSerializerOptions options)
protected override void Initialize(JsonSerializerOptions jsonSerializerOptions)
{
_jsonContext = new BrowsingContextJsonSerializerContext(options);
var browsingContextOptions = new JsonSerializerOptions(jsonSerializerOptions)
{
Converters =
{
new BrowsingContextConverter(BiDi),
new InternalIdConverter(BiDi),
new HandleConverter(BiDi),
new BrowserUserContextConverter(BiDi),
}
};

_jsonContext = new BrowsingContextJsonSerializerContext(browsingContextOptions);
}
}

Expand Down Expand Up @@ -292,4 +304,5 @@ protected override void Initialize(JsonSerializerOptions options)
[JsonSerializable(typeof(NavigationInfo))]
[JsonSerializable(typeof(UserPromptOpenedEventArgs))]
[JsonSerializable(typeof(UserPromptClosedEventArgs))]

internal partial class BrowsingContextJsonSerializerContext : JsonSerializerContext;
Original file line number Diff line number Diff line change
Expand Up @@ -25,52 +25,58 @@ namespace OpenQA.Selenium.BiDi.BrowsingContext;

public sealed class BrowsingContextNetworkModule(BrowsingContext context, NetworkModule networkModule)
{
public async Task<Intercept> InterceptRequestAsync(Func<InterceptedRequest, Task> handler, InterceptRequestOptions? options = null)
public async Task<Interception> InterceptRequestAsync(Func<InterceptedRequest, Task> handler, InterceptRequestOptions? options = null)
{
AddInterceptOptions addInterceptOptions = new(options)
{
Contexts = [context]
};

var intercept = await networkModule.AddInterceptAsync([InterceptPhase.BeforeRequestSent], addInterceptOptions).ConfigureAwait(false);
var interceptResult = await networkModule.AddInterceptAsync([InterceptPhase.BeforeRequestSent], addInterceptOptions).ConfigureAwait(false);

await intercept.OnBeforeRequestSentAsync(
Interception interception = new(context.BiDi, interceptResult.Intercept);

await interception.OnBeforeRequestSentAsync(
async req => await handler(new(req.BiDi, req.Context, req.IsBlocked, req.Navigation, req.RedirectCount, req.Request, req.Timestamp, req.Initiator, req.Intercepts)),
new() { Contexts = [context] }).ConfigureAwait(false);

return intercept;
return interception;
}

public async Task<Intercept> InterceptResponseAsync(Func<InterceptedResponse, Task> handler, InterceptResponseOptions? options = null)
public async Task<Interception> InterceptResponseAsync(Func<InterceptedResponse, Task> handler, InterceptResponseOptions? options = null)
{
AddInterceptOptions addInterceptOptions = new(options)
{
Contexts = [context]
};

var intercept = await networkModule.AddInterceptAsync([InterceptPhase.ResponseStarted], addInterceptOptions).ConfigureAwait(false);
var interceptResult = await networkModule.AddInterceptAsync([InterceptPhase.ResponseStarted], addInterceptOptions).ConfigureAwait(false);

Interception interception = new(context.BiDi, interceptResult.Intercept);

await intercept.OnResponseStartedAsync(
await interception.OnResponseStartedAsync(
async res => await handler(new(res.BiDi, res.Context, res.IsBlocked, res.Navigation, res.RedirectCount, res.Request, res.Timestamp, res.Response, res.Intercepts)),
new() { Contexts = [context] }).ConfigureAwait(false);

return intercept;
return interception;
}

public async Task<Intercept> InterceptAuthAsync(Func<InterceptedAuth, Task> handler, InterceptAuthOptions? options = null)
public async Task<Interception> InterceptAuthAsync(Func<InterceptedAuth, Task> handler, InterceptAuthOptions? options = null)
{
AddInterceptOptions addInterceptOptions = new(options)
{
Contexts = [context]
};

var intercept = await networkModule.AddInterceptAsync([InterceptPhase.AuthRequired], addInterceptOptions).ConfigureAwait(false);
var interceptResult = await networkModule.AddInterceptAsync([InterceptPhase.AuthRequired], addInterceptOptions).ConfigureAwait(false);

Interception interception = new(context.BiDi, interceptResult.Intercept);

await intercept.OnAuthRequiredAsync(
await interception.OnAuthRequiredAsync(
async auth => await handler(new(auth.BiDi, auth.Context, auth.IsBlocked, auth.Navigation, auth.RedirectCount, auth.Request, auth.Timestamp, auth.Response, auth.Intercepts)),
new() { Contexts = [context] }).ConfigureAwait(false);

return intercept;
return interception;
}

public Task<SetCacheBehaviorResult> SetCacheBehaviorAsync(CacheBehavior behavior, BrowsingContextSetCacheBehaviorOptions? options = null)
Expand Down
15 changes: 13 additions & 2 deletions dotnet/src/webdriver/BiDi/Emulation/EmulationModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
// under the License.
// </copyright>

using OpenQA.Selenium.BiDi.Json.Converters;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
Expand Down Expand Up @@ -92,9 +93,18 @@ public async Task<SetGeolocationOverrideResult> SetGeolocationPositionErrorOverr
return await Broker.ExecuteCommandAsync(new SetGeolocationOverrideCommand(@params), options, _jsonContext.SetGeolocationOverrideCommand, _jsonContext.SetGeolocationOverrideResult).ConfigureAwait(false);
}

protected override void Initialize(JsonSerializerOptions options)
protected override void Initialize(JsonSerializerOptions jsonSerializerOptions)
{
_jsonContext = new EmulationJsonSerializerContext(options);
var emulationOptions = new JsonSerializerOptions(jsonSerializerOptions)
{
Converters =
{
new BrowsingContextConverter(BiDi),
new BrowserUserContextConverter(BiDi),
}
};

_jsonContext = new EmulationJsonSerializerContext(emulationOptions);
}
}

Expand All @@ -112,4 +122,5 @@ protected override void Initialize(JsonSerializerOptions options)
[JsonSerializable(typeof(SetScreenOrientationOverrideResult))]
[JsonSerializable(typeof(SetGeolocationOverrideCommand))]
[JsonSerializable(typeof(SetGeolocationOverrideResult))]

internal partial class EmulationJsonSerializerContext : JsonSerializerContext;
9 changes: 8 additions & 1 deletion dotnet/src/webdriver/BiDi/EventArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,21 @@
// under the License.
// </copyright>

using System;
using System.Text.Json.Serialization;

namespace OpenQA.Selenium.BiDi;

public abstract record EventArgs
{
private BiDi? _bidi;

[JsonIgnore]
public BiDi BiDi { get; internal set; }
public BiDi BiDi
{
get => _bidi ?? throw new InvalidOperationException($"{nameof(BiDi)} instance has not been hydrated.");
internal set => _bidi = value;
}
}

public abstract record BrowsingContextEventArgs(BrowsingContext.BrowsingContext Context)
Expand Down
Loading