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 65668dc9924fa..9a096e6805d8e 100644 --- a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs +++ b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs @@ -17,12 +17,21 @@ // under the License. // +using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using System.Text.RegularExpressions; 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")] @@ -37,7 +46,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) @@ -70,14 +79,86 @@ public static LocalValue ConvertFrom(object? value) } } + public static LocalValue FromNode(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(FromNode)); + + case JsonValueKind.Object: + 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}"); + } + } + public abstract record PrimitiveProtocolLocalValue : LocalValue { } + public record BigInt : PrimitiveProtocolLocalValue + { + [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 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; @@ -101,11 +182,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 { @@ -113,6 +211,70 @@ 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 + 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"; + } + + Debug.Assert(options == RegexOptions.None); + + 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;