From 3d4b64553802f3123408bc42c13b2e04667acf2a Mon Sep 17 00:00:00 2001 From: Michael Render Date: Sat, 8 Mar 2025 17:14:39 -0500 Subject: [PATCH 1/7] [dotnet] Add remaining local value subtypes --- .../BiDi/Modules/Script/LocalValue.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs index 65668dc9924fa..2641f747ed17f 100644 --- a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs +++ b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs @@ -17,7 +17,12 @@ // under the License. // +using System; using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace OpenQA.Selenium.BiDi.Modules.Script; @@ -70,11 +75,60 @@ public static LocalValue ConvertFrom(object? value) } } + public static LocalValue ConvertFrom(JsonNode? node) + { + if (node is null) + { + return new Null(); + } + + switch (node.GetValueKind()) + { + case JsonValueKind.True: + return new Boolean(true); + + case JsonValueKind.False: + return new Boolean(false); + + case JsonValueKind.Number: + { + JsonValue value = node.AsValue(); + if (value.TryGetValue(out int intValue)) + { + return new Number(intValue); + } + + if (value.TryGetValue(out double doubleValue) && !double.IsInfinity(doubleValue)) + { + return new Number(doubleValue); + } + + return new BigInt(BigInteger.Parse(value.ToJsonString())); + } + + case JsonValueKind.String: + return new String(node.GetValue()); + + case JsonValueKind.Array: + return new Array(node.AsArray().Select(ConvertFrom)); + + case JsonValueKind.Object: + return new Map(node.AsObject().ToDictionary(m => m.Key, m => ConvertFrom(m.Value))); + + default: + throw new InvalidCastException($"Could not convert node {node}"); + } + } + public abstract record PrimitiveProtocolLocalValue : LocalValue { } + public record BigInt(BigInteger Value) : PrimitiveProtocolLocalValue; + + public record Boolean(bool Value) : PrimitiveProtocolLocalValue; + public record Number(double Value) : PrimitiveProtocolLocalValue { public static explicit operator Number(double n) => new Number(n); From 898a8a4957b500c01defe42395bf598196e20ba1 Mon Sep 17 00:00:00 2001 From: Michael Render Date: Sun, 9 Mar 2025 00:03:13 -0500 Subject: [PATCH 2/7] Add types to LocalValue type descriminator --- dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs index 2641f747ed17f..b22f23b8437ef 100644 --- a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs +++ b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs @@ -28,6 +28,8 @@ namespace OpenQA.Selenium.BiDi.Modules.Script; [JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] +[JsonDerivedType(typeof(Boolean), "boolean")] +[JsonDerivedType(typeof(BigInt), "bigint")] [JsonDerivedType(typeof(Number), "number")] [JsonDerivedType(typeof(String), "string")] [JsonDerivedType(typeof(Null), "null")] From 68e190a4bd6178d739777088a41c5109e73ac659 Mon Sep 17 00:00:00 2001 From: Michael Render Date: Tue, 11 Mar 2025 12:46:07 -0400 Subject: [PATCH 3/7] Add RemoteValue.BigInt anddd JSON serialization support for LocalValues --- .../Json/BiDiJsonSerializerContext.cs | 3 + .../Polymorphic/RemoteValueConverter.cs | 1 + .../BiDi/Modules/Script/LocalValue.cs | 116 ++++++++++++++++-- .../BiDi/Modules/Script/RemoteValue.cs | 23 ++++ 4 files changed, 134 insertions(+), 9 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/Communication/Json/BiDiJsonSerializerContext.cs b/dotnet/src/webdriver/BiDi/Communication/Json/BiDiJsonSerializerContext.cs index 3be20de67c742..ad68bf368614d 100644 --- a/dotnet/src/webdriver/BiDi/Communication/Json/BiDiJsonSerializerContext.cs +++ b/dotnet/src/webdriver/BiDi/Communication/Json/BiDiJsonSerializerContext.cs @@ -32,6 +32,7 @@ namespace OpenQA.Selenium.BiDi.Communication.Json; [JsonSerializable(typeof(Modules.Script.RemoteValue.Number), TypeInfoPropertyName = "Script_RemoteValue_Number")] [JsonSerializable(typeof(Modules.Script.RemoteValue.Boolean), TypeInfoPropertyName = "Script_RemoteValue_Boolean")] +[JsonSerializable(typeof(Modules.Script.RemoteValue.BigInt), TypeInfoPropertyName = "Script_RemoteValue_BigInt")] [JsonSerializable(typeof(Modules.Script.RemoteValue.String), TypeInfoPropertyName = "Script_RemoteValue_String")] [JsonSerializable(typeof(Modules.Script.RemoteValue.Null), TypeInfoPropertyName = "Script_RemoteValue_Null")] [JsonSerializable(typeof(Modules.Script.RemoteValue.Undefined), TypeInfoPropertyName = "Script_RemoteValue_Undefined")] @@ -141,6 +142,8 @@ namespace OpenQA.Selenium.BiDi.Communication.Json; [JsonSerializable(typeof(Modules.Network.AuthRequiredEventArgs))] [JsonSerializable(typeof(Modules.Script.Channel), TypeInfoPropertyName = "Script_Channel")] +[JsonSerializable(typeof(Modules.Script.LocalValue.Boolean), TypeInfoPropertyName = "Script_LocalValue_Boolean")] +[JsonSerializable(typeof(Modules.Script.LocalValue.BigInt), TypeInfoPropertyName = "Script_LocalValue_BigInt")] [JsonSerializable(typeof(Modules.Script.LocalValue.String), TypeInfoPropertyName = "Script_LocalValue_String")] [JsonSerializable(typeof(Modules.Script.Target.Realm), TypeInfoPropertyName = "Script_Target_Realm")] [JsonSerializable(typeof(Modules.Script.Target.Context), TypeInfoPropertyName = "Script_Target_Context")] diff --git a/dotnet/src/webdriver/BiDi/Communication/Json/Converters/Polymorphic/RemoteValueConverter.cs b/dotnet/src/webdriver/BiDi/Communication/Json/Converters/Polymorphic/RemoteValueConverter.cs index 88539ce7b55fd..227bd3457fef1 100644 --- a/dotnet/src/webdriver/BiDi/Communication/Json/Converters/Polymorphic/RemoteValueConverter.cs +++ b/dotnet/src/webdriver/BiDi/Communication/Json/Converters/Polymorphic/RemoteValueConverter.cs @@ -40,6 +40,7 @@ internal class RemoteValueConverter : JsonConverter { "number" => jsonDocument.Deserialize(options), "boolean" => jsonDocument.Deserialize(options), + "bigint" => jsonDocument.Deserialize(options), "string" => jsonDocument.Deserialize(options), "null" => jsonDocument.Deserialize(options), "undefined" => jsonDocument.Deserialize(options), diff --git a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs index b22f23b8437ef..73426d527b713 100644 --- a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs +++ b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs @@ -24,6 +24,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using System.Text.RegularExpressions; namespace OpenQA.Selenium.BiDi.Modules.Script; @@ -44,7 +45,7 @@ namespace OpenQA.Selenium.BiDi.Modules.Script; public abstract record LocalValue { public static implicit operator LocalValue(int value) { return new Number(value); } - public static implicit operator LocalValue(string value) { return new String(value); } + public static implicit operator LocalValue(string? value) { return value is null ? new Null() : new String(value); } // TODO: Extend converting from types public static LocalValue ConvertFrom(object? value) @@ -77,7 +78,7 @@ public static LocalValue ConvertFrom(object? value) } } - public static LocalValue ConvertFrom(JsonNode? node) + public static LocalValue FromNode(JsonNode? node) { if (node is null) { @@ -112,28 +113,41 @@ public static LocalValue ConvertFrom(JsonNode? node) return new String(node.GetValue()); case JsonValueKind.Array: - return new Array(node.AsArray().Select(ConvertFrom)); + return new Array(node.AsArray().Select(FromNode)); case JsonValueKind.Object: - return new Map(node.AsObject().ToDictionary(m => m.Key, m => ConvertFrom(m.Value))); + return new Map(node.AsObject().ToDictionary(m => m.Key, m => FromNode(m.Value))); default: throw new InvalidCastException($"Could not convert node {node}"); } } - public abstract record PrimitiveProtocolLocalValue : LocalValue + public abstract record PrimitiveProtocolLocalValue : LocalValue; + + public record BigInt : PrimitiveProtocolLocalValue { + [JsonIgnore] + public BigInteger Value { get; } - } + [JsonInclude] + [JsonPropertyName("value")] + public string ValueAsString => Value.ToString(); - public record BigInt(BigInteger Value) : PrimitiveProtocolLocalValue; + public BigInt(BigInteger value) + { + Value = value; + } + } public record Boolean(bool Value) : PrimitiveProtocolLocalValue; public record Number(double Value) : PrimitiveProtocolLocalValue { public static explicit operator Number(double n) => new Number(n); + public static Number PositiveInfinity { get; } = new Number(double.PositiveInfinity); + public static Number NegativeInfinity { get; } = new Number(double.NegativeInfinity); + public static Number NaN { get; } = new Number(double.NaN); } public record String(string Value) : PrimitiveProtocolLocalValue; @@ -157,11 +171,28 @@ public record ChannelProperties(Script.Channel Channel) public record Array(IEnumerable Value) : LocalValue; - public record Date(string Value) : LocalValue; + public record Date(string Value) : LocalValue + { + public static Date FromDateTime(DateTime value) + { + return new Date(value.ToString("o")); + } + } public record Map(IDictionary Value) : LocalValue; // seems to implement IDictionary - public record Object(IEnumerable> Value) : LocalValue; + public record Object(IEnumerable> Value) : LocalValue + { + public static Object FromDictionary(IDictionary values) + { + return new Object([.. values.Select(PairAsList)]); + } + + private static IEnumerable PairAsList(KeyValuePair pair) + { + return [new String(pair.Key), pair.Value ?? new Null()]; + } + } public record RegExp(RegExp.RegExpValue Value) : LocalValue { @@ -169,6 +200,73 @@ public record RegExpValue(string Pattern) { public string? Flags { get; set; } } + + /// + /// Converts a .NET Regex into a BiDi Regex + /// + /// A .NET Regex. + /// A BiDi Regex. + /// + /// Note that the .NET regular expression engine does not work the same as the JavaScript engine. + /// To minimize the differences between the two engines, it is recommended to enabled the option. + /// + public static RegExp FromRegex(Regex regex) + { + RegexOptions options = regex.Options; + + if (options == RegexOptions.None) + { + return new RegExp(new RegExpValue(regex.ToString())); + } + + string flags = string.Empty; + + const RegexOptions NonBacktracking = (RegexOptions)1024; +#if NET8_0_OR_GREATER + System.Diagnostics.Debug.Assert(NonBacktracking == RegexOptions.NonBacktracking); +#endif + + const RegexOptions NonApplicableOptions = RegexOptions.Compiled | NonBacktracking; + + const RegexOptions UnsupportedOptions = + RegexOptions.ExplicitCapture | + RegexOptions.IgnorePatternWhitespace | + RegexOptions.RightToLeft | + RegexOptions.CultureInvariant; + + options &= ~NonApplicableOptions; + + if ((options & UnsupportedOptions) != 0) + { + throw new NotSupportedException($"The selected RegEx options are not supported in BiDi: {options & UnsupportedOptions}"); + } + + if ((options & RegexOptions.IgnoreCase) != 0) + { + flags += "i"; + options = options & ~RegexOptions.IgnoreCase; + } + + if ((options & RegexOptions.Multiline) != 0) + { + options = options & ~RegexOptions.Multiline; + flags += "m"; + } + + if ((options & RegexOptions.Singleline) != 0) + { + options = options & ~RegexOptions.Singleline; + flags += "s"; + } + + if (options != RegexOptions.None) + { + // Consider logging? + } + + return new RegExp(new RegExpValue(regex.ToString()) { Flags = flags }); + + } } public record Set(IEnumerable Value) : LocalValue; diff --git a/dotnet/src/webdriver/BiDi/Modules/Script/RemoteValue.cs b/dotnet/src/webdriver/BiDi/Modules/Script/RemoteValue.cs index 8ff737adf44cf..6c36abb03ea49 100644 --- a/dotnet/src/webdriver/BiDi/Modules/Script/RemoteValue.cs +++ b/dotnet/src/webdriver/BiDi/Modules/Script/RemoteValue.cs @@ -19,6 +19,7 @@ using System; using System.Collections.Generic; +using System.Numerics; using System.Text.Json; using System.Text.Json.Serialization; @@ -28,6 +29,7 @@ namespace OpenQA.Selenium.BiDi.Modules.Script; //[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] //[JsonDerivedType(typeof(Number), "number")] //[JsonDerivedType(typeof(Boolean), "boolean")] +//[JsonDerivedType(typeof(BigInt), "bigint")] //[JsonDerivedType(typeof(String), "string")] //[JsonDerivedType(typeof(Null), "null")] //[JsonDerivedType(typeof(Undefined), "undefined")] @@ -95,6 +97,27 @@ public record Number(double Value) : PrimitiveProtocolRemoteValue; public record Boolean(bool Value) : PrimitiveProtocolRemoteValue; + public record BigInt : PrimitiveProtocolRemoteValue + { + [JsonIgnore] + public BigInteger Value { get; } + + [JsonInclude] + [JsonPropertyName("value")] + public string ValueAsString => Value.ToString(); + + public BigInt(BigInteger value) + { + Value = value; + } + + [JsonConstructor] + internal BigInt(string valueAsString) + { + Value = BigInteger.Parse(valueAsString); + } + } + public record String(string Value) : PrimitiveProtocolRemoteValue; public record Null : PrimitiveProtocolRemoteValue; From 882e13a179bcdca07f6fcab3d3f4d8a8b7c7eda6 Mon Sep 17 00:00:00 2001 From: Michael Render Date: Tue, 11 Mar 2025 12:47:35 -0400 Subject: [PATCH 4/7] minimize diffs --- dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs index 73426d527b713..c50833079248c 100644 --- a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs +++ b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs @@ -123,7 +123,10 @@ public static LocalValue FromNode(JsonNode? node) } } - public abstract record PrimitiveProtocolLocalValue : LocalValue; + public abstract record PrimitiveProtocolLocalValue : LocalValue + { + + } public record BigInt : PrimitiveProtocolLocalValue { From c10c54d4e7ef0fa18c590c5f35b3a6fcfa703370 Mon Sep 17 00:00:00 2001 From: Michael Render Date: Tue, 11 Mar 2025 12:48:34 -0400 Subject: [PATCH 5/7] Add deserialization ctor to LocalValue.BigInt --- dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs index c50833079248c..079b8b5911750 100644 --- a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs +++ b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs @@ -141,6 +141,12 @@ public BigInt(BigInteger value) { Value = value; } + + [JsonConstructor] + internal BigInt(string valueAsString) + { + Value = BigInteger.Parse(valueAsString); + } } public record Boolean(bool Value) : PrimitiveProtocolLocalValue; From 86f9171b221c139aa9b69916f264b5affc850449 Mon Sep 17 00:00:00 2001 From: Michael Render Date: Tue, 11 Mar 2025 12:50:42 -0400 Subject: [PATCH 6/7] Turn conditional into assertion --- dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs index 079b8b5911750..0dfa0634db91c 100644 --- a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs +++ b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs @@ -19,6 +19,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Numerics; using System.Text.Json; @@ -232,7 +233,7 @@ public static RegExp FromRegex(Regex regex) const RegexOptions NonBacktracking = (RegexOptions)1024; #if NET8_0_OR_GREATER - System.Diagnostics.Debug.Assert(NonBacktracking == RegexOptions.NonBacktracking); + Debug.Assert(NonBacktracking == RegexOptions.NonBacktracking); #endif const RegexOptions NonApplicableOptions = RegexOptions.Compiled | NonBacktracking; @@ -268,10 +269,7 @@ public static RegExp FromRegex(Regex regex) flags += "s"; } - if (options != RegexOptions.None) - { - // Consider logging? - } + Debug.Assert(options == RegexOptions.None); return new RegExp(new RegExpValue(regex.ToString()) { Flags = flags }); From 5c0c569c85fef7ee1c39948c85d7ab202d596448 Mon Sep 17 00:00:00 2001 From: Michael Render Date: Tue, 11 Mar 2025 13:51:53 -0400 Subject: [PATCH 7/7] Use `Object` instead of `Map` for `LocalValue.FromNode` --- dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs index 0dfa0634db91c..9a096e6805d8e 100644 --- a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs +++ b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs @@ -117,7 +117,8 @@ public static LocalValue FromNode(JsonNode? node) return new Array(node.AsArray().Select(FromNode)); case JsonValueKind.Object: - return new Map(node.AsObject().ToDictionary(m => m.Key, m => FromNode(m.Value))); + Dictionary values = node.AsObject().ToDictionary(node => node.Key, node => FromNode(node.Value)); + return Object.FromDictionary(values); default: throw new InvalidCastException($"Could not convert node {node}");