From 95c8707a5e76d7bfdf7923f2a00b9ef9329e5493 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Thu, 11 Dec 2025 19:26:46 +0300 Subject: [PATCH 1/8] Remove polumorphic serialization --- .../src/webdriver/BiDi/Script/LocalValue.cs | 88 +++++++++++++------ 1 file changed, 60 insertions(+), 28 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/Script/LocalValue.cs b/dotnet/src/webdriver/BiDi/Script/LocalValue.cs index 809d54d5410e5..13087e3dab06d 100644 --- a/dotnet/src/webdriver/BiDi/Script/LocalValue.cs +++ b/dotnet/src/webdriver/BiDi/Script/LocalValue.cs @@ -17,6 +17,7 @@ // under the License. // +using OpenQA.Selenium.BiDi.Json.Converters; using System; using System.Collections; using System.Collections.Generic; @@ -24,24 +25,9 @@ using System.Numerics; using System.Text.Json.Serialization; using System.Text.RegularExpressions; -using OpenQA.Selenium.BiDi.Json.Converters; namespace OpenQA.Selenium.BiDi.Script; -[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] -[JsonDerivedType(typeof(NumberLocalValue), "number")] -[JsonDerivedType(typeof(StringLocalValue), "string")] -[JsonDerivedType(typeof(NullLocalValue), "null")] -[JsonDerivedType(typeof(UndefinedLocalValue), "undefined")] -[JsonDerivedType(typeof(BooleanLocalValue), "boolean")] -[JsonDerivedType(typeof(BigIntLocalValue), "bigint")] -[JsonDerivedType(typeof(ChannelLocalValue), "channel")] -[JsonDerivedType(typeof(ArrayLocalValue), "array")] -[JsonDerivedType(typeof(DateLocalValue), "date")] -[JsonDerivedType(typeof(MapLocalValue), "map")] -[JsonDerivedType(typeof(ObjectLocalValue), "object")] -[JsonDerivedType(typeof(RegExpLocalValue), "regexp")] -[JsonDerivedType(typeof(SetLocalValue), "set")] public abstract record LocalValue { public static implicit operator LocalValue(bool? value) { return ConvertFrom(value); } @@ -285,34 +271,80 @@ public abstract record PrimitiveProtocolLocalValue : LocalValue; public sealed record NumberLocalValue([property: JsonConverter(typeof(SpecialNumberConverter))] double Value) : PrimitiveProtocolLocalValue { + [JsonInclude] + internal string Type { get; } = "number"; + public static explicit operator NumberLocalValue(double n) => new NumberLocalValue(n); } -public sealed record StringLocalValue(string Value) : PrimitiveProtocolLocalValue; +public sealed record StringLocalValue(string Value) : PrimitiveProtocolLocalValue +{ + [JsonInclude] + internal string Type { get; } = "string"; +} -public sealed record NullLocalValue : PrimitiveProtocolLocalValue; +public sealed record NullLocalValue : PrimitiveProtocolLocalValue +{ + [JsonInclude] + internal string Type { get; } = "null"; +} -public sealed record UndefinedLocalValue : PrimitiveProtocolLocalValue; +public sealed record UndefinedLocalValue : PrimitiveProtocolLocalValue +{ + [JsonInclude] + internal string Type { get; } = "undefined"; +} -public sealed record BooleanLocalValue(bool Value) : PrimitiveProtocolLocalValue; +public sealed record BooleanLocalValue(bool Value) : PrimitiveProtocolLocalValue +{ + [JsonInclude] + internal string Type { get; } = "boolean"; +} -public sealed record BigIntLocalValue(string Value) : PrimitiveProtocolLocalValue; +public sealed record BigIntLocalValue(string Value) : PrimitiveProtocolLocalValue +{ + [JsonInclude] + internal string Type { get; } = "bigint"; +} public sealed record ChannelLocalValue(ChannelProperties Value) : LocalValue { - // AddPreloadScript takes arguments typed as ChannelLocalValue but still requires "type":"channel" [JsonInclude] - internal string Type => "channel"; + internal string Type { get; } = "channel"; } -public sealed record ArrayLocalValue(IEnumerable Value) : LocalValue; +public sealed record ArrayLocalValue(IEnumerable Value) : LocalValue +{ + [JsonInclude] + internal string Type { get; } = "array"; +} -public sealed record DateLocalValue(string Value) : LocalValue; +public sealed record DateLocalValue(string Value) : LocalValue +{ + [JsonInclude] + internal string Type { get; } = "date"; +} -public sealed record MapLocalValue(IEnumerable> Value) : LocalValue; +public sealed record MapLocalValue(IEnumerable> Value) : LocalValue +{ + [JsonInclude] + internal string Type { get; } = "map"; +} -public sealed record ObjectLocalValue(IEnumerable> Value) : LocalValue; +public sealed record ObjectLocalValue(IEnumerable> Value) : LocalValue +{ + [JsonInclude] + internal string Type { get; } = "object"; +} -public sealed record RegExpLocalValue(RegExpValue Value) : LocalValue; +public sealed record RegExpLocalValue(RegExpValue Value) : LocalValue +{ + [JsonInclude] + internal string Type { get; } = "regexp"; +} -public sealed record SetLocalValue(IEnumerable Value) : LocalValue; +public sealed record SetLocalValue(IEnumerable Value) : LocalValue +{ + [JsonInclude] + internal string Type { get; } = "set"; +} From 590c45cc5dd90146d9e891ef8d427f08e17c5560 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Thu, 11 Dec 2025 19:31:33 +0300 Subject: [PATCH 2/8] Added RemoteReferenceLocalValue --- dotnet/src/webdriver/BiDi/Script/LocalValue.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/dotnet/src/webdriver/BiDi/Script/LocalValue.cs b/dotnet/src/webdriver/BiDi/Script/LocalValue.cs index 13087e3dab06d..7195e401150fd 100644 --- a/dotnet/src/webdriver/BiDi/Script/LocalValue.cs +++ b/dotnet/src/webdriver/BiDi/Script/LocalValue.cs @@ -267,6 +267,18 @@ private static LocalValue ReflectionBasedConvertFrom(object? value) } } +public abstract record RemoteReferenceLocalValue : LocalValue, IRemoteReference; + +public sealed record SharedReferenceLocalValue(string SharedId) : RemoteReferenceLocalValue, ISharedReference +{ + public Handle? Handle { get; set; } +} + +public sealed record RemoteObjectReferenceLocalValue(Handle Handle) : RemoteReferenceLocalValue, IRemoteObjectReference +{ + public string? SharedId { get; set; } +} + public abstract record PrimitiveProtocolLocalValue : LocalValue; public sealed record NumberLocalValue([property: JsonConverter(typeof(SpecialNumberConverter))] double Value) : PrimitiveProtocolLocalValue From 54de706e9ae17a2b80faaca3935f1cbfe7f18dd2 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Thu, 11 Dec 2025 20:22:32 +0300 Subject: [PATCH 3/8] Return back polymorphic semantic without discriminator --- dotnet/src/webdriver/BiDi/Script/LocalValue.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dotnet/src/webdriver/BiDi/Script/LocalValue.cs b/dotnet/src/webdriver/BiDi/Script/LocalValue.cs index 7195e401150fd..c50bd6eec7ca2 100644 --- a/dotnet/src/webdriver/BiDi/Script/LocalValue.cs +++ b/dotnet/src/webdriver/BiDi/Script/LocalValue.cs @@ -28,6 +28,20 @@ namespace OpenQA.Selenium.BiDi.Script; +[JsonPolymorphic] +[JsonDerivedType(typeof(NumberLocalValue))] +[JsonDerivedType(typeof(StringLocalValue))] +[JsonDerivedType(typeof(NullLocalValue))] +[JsonDerivedType(typeof(UndefinedLocalValue))] +[JsonDerivedType(typeof(BooleanLocalValue))] +[JsonDerivedType(typeof(BigIntLocalValue))] +[JsonDerivedType(typeof(ChannelLocalValue))] +[JsonDerivedType(typeof(ArrayLocalValue))] +[JsonDerivedType(typeof(DateLocalValue))] +[JsonDerivedType(typeof(MapLocalValue))] +[JsonDerivedType(typeof(ObjectLocalValue))] +[JsonDerivedType(typeof(RegExpLocalValue))] +[JsonDerivedType(typeof(SetLocalValue))] public abstract record LocalValue { public static implicit operator LocalValue(bool? value) { return ConvertFrom(value); } From 111d55acd336b0545ea12ef8e080786d4581f81a Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Thu, 11 Dec 2025 20:33:41 +0300 Subject: [PATCH 4/8] Manually control discriminator per LoocalValue --- .../src/webdriver/BiDi/Script/LocalValue.cs | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/Script/LocalValue.cs b/dotnet/src/webdriver/BiDi/Script/LocalValue.cs index c50bd6eec7ca2..0f4a0669023e7 100644 --- a/dotnet/src/webdriver/BiDi/Script/LocalValue.cs +++ b/dotnet/src/webdriver/BiDi/Script/LocalValue.cs @@ -29,6 +29,8 @@ namespace OpenQA.Selenium.BiDi.Script; [JsonPolymorphic] +[JsonDerivedType(typeof(SharedReferenceLocalValue))] +[JsonDerivedType(typeof(RemoteObjectReferenceLocalValue))] [JsonDerivedType(typeof(NumberLocalValue))] [JsonDerivedType(typeof(StringLocalValue))] [JsonDerivedType(typeof(NullLocalValue))] @@ -279,6 +281,9 @@ private static LocalValue ReflectionBasedConvertFrom(object? value) return new ObjectLocalValue(values); } + + [JsonInclude] + internal abstract string Type { get; } } public abstract record RemoteReferenceLocalValue : LocalValue, IRemoteReference; @@ -286,91 +291,82 @@ public abstract record RemoteReferenceLocalValue : LocalValue, IRemoteReference; public sealed record SharedReferenceLocalValue(string SharedId) : RemoteReferenceLocalValue, ISharedReference { public Handle? Handle { get; set; } + + internal override string Type { get; } = null!; } public sealed record RemoteObjectReferenceLocalValue(Handle Handle) : RemoteReferenceLocalValue, IRemoteObjectReference { public string? SharedId { get; set; } + + internal override string Type { get; } = null!; } public abstract record PrimitiveProtocolLocalValue : LocalValue; public sealed record NumberLocalValue([property: JsonConverter(typeof(SpecialNumberConverter))] double Value) : PrimitiveProtocolLocalValue { - [JsonInclude] - internal string Type { get; } = "number"; + internal override string Type { get; } = "number"; public static explicit operator NumberLocalValue(double n) => new NumberLocalValue(n); } public sealed record StringLocalValue(string Value) : PrimitiveProtocolLocalValue { - [JsonInclude] - internal string Type { get; } = "string"; + internal override string Type { get; } = "string"; } public sealed record NullLocalValue : PrimitiveProtocolLocalValue { - [JsonInclude] - internal string Type { get; } = "null"; + internal override string Type { get; } = "null"; } public sealed record UndefinedLocalValue : PrimitiveProtocolLocalValue { - [JsonInclude] - internal string Type { get; } = "undefined"; + internal override string Type { get; } = "undefined"; } public sealed record BooleanLocalValue(bool Value) : PrimitiveProtocolLocalValue { - [JsonInclude] - internal string Type { get; } = "boolean"; + internal override string Type { get; } = "boolean"; } public sealed record BigIntLocalValue(string Value) : PrimitiveProtocolLocalValue { - [JsonInclude] - internal string Type { get; } = "bigint"; + internal override string Type { get; } = "bigint"; } public sealed record ChannelLocalValue(ChannelProperties Value) : LocalValue { - [JsonInclude] - internal string Type { get; } = "channel"; + internal override string Type { get; } = "channel"; } public sealed record ArrayLocalValue(IEnumerable Value) : LocalValue { - [JsonInclude] - internal string Type { get; } = "array"; + internal override string Type { get; } = "array"; } public sealed record DateLocalValue(string Value) : LocalValue { - [JsonInclude] - internal string Type { get; } = "date"; + internal override string Type { get; } = "date"; } public sealed record MapLocalValue(IEnumerable> Value) : LocalValue { - [JsonInclude] - internal string Type { get; } = "map"; + internal override string Type { get; } = "map"; } public sealed record ObjectLocalValue(IEnumerable> Value) : LocalValue { - [JsonInclude] - internal string Type { get; } = "object"; + internal override string Type { get; } = "object"; } public sealed record RegExpLocalValue(RegExpValue Value) : LocalValue { - [JsonInclude] - internal string Type { get; } = "regexp"; + internal override string Type { get; } = "regexp"; } public sealed record SetLocalValue(IEnumerable Value) : LocalValue { - [JsonInclude] - internal string Type { get; } = "set"; + internal override string Type { get; } = "set"; } From 5c9586acf514a110e2ac1256b54993e7d2efdf79 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Thu, 11 Dec 2025 20:44:27 +0300 Subject: [PATCH 5/8] Add test for SharedReferenceLocalValue --- .../BiDi/Script/CallFunctionLocalValueTest.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/dotnet/test/common/BiDi/Script/CallFunctionLocalValueTest.cs b/dotnet/test/common/BiDi/Script/CallFunctionLocalValueTest.cs index 4ec70c96060a9..b46bf75823c9b 100644 --- a/dotnet/test/common/BiDi/Script/CallFunctionLocalValueTest.cs +++ b/dotnet/test/common/BiDi/Script/CallFunctionLocalValueTest.cs @@ -363,4 +363,25 @@ public async Task CanCallFunctionWithArgumentSet() Assert.That(result, Is.TypeOf(), $"Call was not successful: {result}"); } + + [Test] + public async Task CanCallFunctionWithSharedReferenceLocalValue() + { + // Navigate to a page with a known element + driver.Url = UrlBuilder.WhereIs("bidi/logEntryAdded.html"); + + var node = (await context.LocateNodesAsync(new BrowsingContext.CssLocator("#consoleLog"))).Nodes[0]; + + var arg = new SharedReferenceLocalValue(node.SharedId); + + var result = await context.Script.CallFunctionAsync($$""" + (el) => { + if (!(el instanceof Element) || el.id !== 'consoleLog') { + throw new Error("Assert failed: " + (el && el.id)); + } + } + """, false, new() { Arguments = [arg] }); + + Assert.That(result, Is.TypeOf(), $"Call was not successful: {result}"); + } } From 4aaa25ec4cb90afdbf8147c12e170f462fca2a93 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Thu, 11 Dec 2025 21:05:00 +0300 Subject: [PATCH 6/8] Add test for RemoteObjectReferenceLocalValue --- .../src/webdriver/BiDi/Script/RemoteValue.cs | 6 ++++-- .../BiDi/Script/CallFunctionLocalValueTest.cs | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs b/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs index d7d54a147d448..6b06b8fa68459 100644 --- a/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs +++ b/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs @@ -21,8 +21,6 @@ using OpenQA.Selenium.BiDi.Json.Converters.Polymorphic; using System; using System.Collections.Generic; -using System.Numerics; -using System.Text.Json; using System.Text.Json.Serialization; namespace OpenQA.Selenium.BiDi.Script; @@ -90,6 +88,10 @@ public abstract record RemoteValue { return (TResult)(((StringRemoteValue)this).Value as object); } + else if (type == typeof(ObjectRemoteValue)) + { + return (TResult)(((ObjectRemoteValue)this) as object); + } else if (type is object) { // :) diff --git a/dotnet/test/common/BiDi/Script/CallFunctionLocalValueTest.cs b/dotnet/test/common/BiDi/Script/CallFunctionLocalValueTest.cs index b46bf75823c9b..02bb4a0069667 100644 --- a/dotnet/test/common/BiDi/Script/CallFunctionLocalValueTest.cs +++ b/dotnet/test/common/BiDi/Script/CallFunctionLocalValueTest.cs @@ -384,4 +384,22 @@ public async Task CanCallFunctionWithSharedReferenceLocalValue() Assert.That(result, Is.TypeOf(), $"Call was not successful: {result}"); } + + [Test] + public async Task CanCallFunctionWithRemoteObjectReferenceLocalValue() + { + ObjectRemoteValue objectRemoteValue = await context.Script.CallFunctionAsync("() => ({ a: 42 })", true, new() { ResultOwnership = ResultOwnership.Root }); + + var arg = new RemoteObjectReferenceLocalValue(objectRemoteValue.Handle!); + + var result = await context.Script.CallFunctionAsync($$""" + (refObj) => { + if (typeof refObj !== 'object' || refObj.a !== 42) { + throw new Error("Assert failed: ref a=" + (refObj && refObj.a)); + } + } + """, false, new() { Arguments = [arg] }); + + Assert.That(result, Is.TypeOf(), $"Call was not successful: {result}"); + } } From f937c59bb5df8c7108b473f9d188ac1fe659ff88 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Thu, 11 Dec 2025 21:06:41 +0300 Subject: [PATCH 7/8] Wrap args --- dotnet/test/common/BiDi/Script/CallFunctionLocalValueTest.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dotnet/test/common/BiDi/Script/CallFunctionLocalValueTest.cs b/dotnet/test/common/BiDi/Script/CallFunctionLocalValueTest.cs index 02bb4a0069667..2fad98b20f9e4 100644 --- a/dotnet/test/common/BiDi/Script/CallFunctionLocalValueTest.cs +++ b/dotnet/test/common/BiDi/Script/CallFunctionLocalValueTest.cs @@ -388,7 +388,10 @@ public async Task CanCallFunctionWithSharedReferenceLocalValue() [Test] public async Task CanCallFunctionWithRemoteObjectReferenceLocalValue() { - ObjectRemoteValue objectRemoteValue = await context.Script.CallFunctionAsync("() => ({ a: 42 })", true, new() { ResultOwnership = ResultOwnership.Root }); + ObjectRemoteValue objectRemoteValue = await context.Script.CallFunctionAsync( + "() => ({ a: 42 })", + true, + new() { ResultOwnership = ResultOwnership.Root }); var arg = new RemoteObjectReferenceLocalValue(objectRemoteValue.Handle!); From c8bf90fad54a55e869fee0987c2bec9742645095 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Thu, 11 Dec 2025 21:32:26 +0300 Subject: [PATCH 8/8] Handle native derived types from RemoteValue --- dotnet/src/webdriver/BiDi/Script/RemoteValue.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs b/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs index 6b06b8fa68459..ea03b3ab345df 100644 --- a/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs +++ b/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs @@ -76,6 +76,10 @@ public abstract record RemoteValue { var type = typeof(TResult); + if (typeof(RemoteValue).IsAssignableFrom(type)) // handle native derived types + { + return (TResult)(this as object); + } if (type == typeof(bool)) { return (TResult)(Convert.ToBoolean(((BooleanRemoteValue)this).Value) as object); @@ -88,10 +92,6 @@ public abstract record RemoteValue { return (TResult)(((StringRemoteValue)this).Value as object); } - else if (type == typeof(ObjectRemoteValue)) - { - return (TResult)(((ObjectRemoteValue)this) as object); - } else if (type is object) { // :)