diff --git a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs index 19be05efaaf18..2e458c0377920 100644 --- a/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs +++ b/dotnet/src/webdriver/BiDi/Modules/Script/LocalValue.cs @@ -18,10 +18,12 @@ // using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Text.Json.Serialization; +using System.Text.RegularExpressions; namespace OpenQA.Selenium.BiDi.Modules.Script; @@ -41,10 +43,11 @@ namespace OpenQA.Selenium.BiDi.Modules.Script; [JsonDerivedType(typeof(SetLocalValue), "set")] public abstract record LocalValue { - public static implicit operator LocalValue(bool? value) { return value is bool b ? new BooleanLocalValue(b) : new NullLocalValue(); } - public static implicit operator LocalValue(int? value) { return value is int i ? new NumberLocalValue(i) : new NullLocalValue(); } - public static implicit operator LocalValue(double? value) { return value is double d ? new NumberLocalValue(d) : new NullLocalValue(); } - public static implicit operator LocalValue(string? value) { return value is null ? new NullLocalValue() : new StringLocalValue(value); } + public static implicit operator LocalValue(bool? value) { return ConvertFrom(value); } + public static implicit operator LocalValue(int? value) { return ConvertFrom(value); } + public static implicit operator LocalValue(double? value) { return ConvertFrom(value); } + public static implicit operator LocalValue(string? value) { return ConvertFrom(value); } + public static implicit operator LocalValue(DateTimeOffset? value) { return ConvertFrom(value); } // TODO: Extend converting from types public static LocalValue ConvertFrom(object? value) @@ -58,86 +61,222 @@ public static LocalValue ConvertFrom(object? value) return new NullLocalValue(); case bool b: - return new BooleanLocalValue(b); + return ConvertFrom(b); case int i: - return new NumberLocalValue(i); + return ConvertFrom(i); case double d: - return new NumberLocalValue(d); + return ConvertFrom(d); case long l: - return new NumberLocalValue(l); + return ConvertFrom(l); - case DateTime dt: - return new DateLocalValue(dt.ToString("o")); + case DateTimeOffset dt: + return ConvertFrom(dt); case BigInteger bigInt: - return new BigIntLocalValue(bigInt.ToString()); + return ConvertFrom(bigInt); case string str: - return new StringLocalValue(str); + return ConvertFrom(str); - case IDictionary dictionary: + case Regex regex: + return ConvertFrom(regex); + + case { } when value.GetType().GetInterfaces() + .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ISet<>)): { - var bidiObject = new List>(dictionary.Count); - foreach (var item in dictionary) - { - bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]); - } + IEnumerable set = (IEnumerable)value; - return new ObjectLocalValue(bidiObject); - } + List setValues = []; - case IDictionary dictionary: - { - var bidiObject = new List>(dictionary.Count); - foreach (var item in dictionary) + foreach (var obj in set) { - bidiObject.Add([new StringLocalValue(item.Key), ConvertFrom(item.Value)]); + setValues.Add(ConvertFrom(obj)); } - return new ObjectLocalValue(bidiObject); + return new SetLocalValue(setValues); } - case IDictionary dictionary: - { - var bidiObject = new List>(dictionary.Count); - foreach (var item in dictionary) - { - bidiObject.Add([ConvertFrom(item.Key), ConvertFrom(item.Value)]); - } + case IDictionary dictionary: + return ConvertFrom(dictionary); - return new MapLocalValue(bidiObject); - } + case IEnumerable enumerable: + return ConvertFrom(enumerable); - case IEnumerable list: - return new ArrayLocalValue(list.Select(ConvertFrom).ToList()); + default: + return ReflectionBasedConvertFrom(value); + } + } - case object: - { - const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance; + public static LocalValue ConvertFrom(bool? value) + { + if (value is bool b) + { + return new BooleanLocalValue(b); + } - var properties = value.GetType().GetProperties(Flags); + return new NullLocalValue(); + } - var values = new List>(properties.Length); - foreach (var property in properties) - { - object? propertyValue; - try - { - propertyValue = property.GetValue(value); - } - catch (Exception ex) - { - throw new BiDiException($"Could not retrieve property {property.Name} from {property.DeclaringType}", ex); - } - values.Add([property.Name, ConvertFrom(propertyValue)]); - } + public static LocalValue ConvertFrom(int? value) + { + if (value is int b) + { + return new NumberLocalValue(b); + } - return new ObjectLocalValue(values); - } + return new NullLocalValue(); + } + + public static LocalValue ConvertFrom(double? value) + { + if (value is double b) + { + return new NumberLocalValue(b); + } + + return new NullLocalValue(); + } + + public static LocalValue ConvertFrom(long? value) + { + if (value is long b) + { + return new NumberLocalValue(b); + } + + return new NullLocalValue(); + } + + public static LocalValue ConvertFrom(string? value) + { + if (value is not null) + { + return new StringLocalValue(value); + } + + return new NullLocalValue(); + } + + /// + /// 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 LocalValue ConvertFrom(Regex? regex) + { + if (regex is null) + { + return new NullLocalValue(); } + + string? flags = RegExpValue.GetRegExpFlags(regex.Options); + + return new RegExpLocalValue(new RegExpValue(regex.ToString()) { Flags = flags }); + } + + public static LocalValue ConvertFrom(DateTimeOffset? value) + { + if (value is null) + { + return new NullLocalValue(); + } + + return new DateLocalValue(value.Value.ToString("o")); + } + + public static LocalValue ConvertFrom(BigInteger? value) + { + if (value is not null) + { + return new BigIntLocalValue(value.Value.ToString()); + } + + return new NullLocalValue(); + } + + public static LocalValue ConvertFrom(IEnumerable? value) + { + if (value is null) + { + return new NullLocalValue(); + } + + List list = []; + + foreach (var element in value) + { + list.Add(ConvertFrom(element)); + } + + return new ArrayLocalValue(list); + } + + public static LocalValue ConvertFrom(IDictionary? value) + { + if (value is null) + { + return new NullLocalValue(); + } + + var bidiObject = new List>(value.Count); + + foreach (var key in value.Keys) + { + bidiObject.Add([ConvertFrom(key), ConvertFrom(value[key])]); + } + + return new MapLocalValue(bidiObject); + } + + public static LocalValue ConvertFrom(ISet? value) + { + if (value is null) + { + return new NullLocalValue(); + } + + LocalValue[] convertedValues = [.. value.Select(x => ConvertFrom(x))]; + + return new SetLocalValue(convertedValues); + } + + private static LocalValue ReflectionBasedConvertFrom(object? value) + { + if (value is null) + { + return new NullLocalValue(); + } + + const System.Reflection.BindingFlags Flags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance; + + System.Reflection.PropertyInfo[] properties = value.GetType().GetProperties(Flags); + + var values = new List>(properties.Length); + + foreach (System.Reflection.PropertyInfo? property in properties) + { + object? propertyValue; + + try + { + propertyValue = property.GetValue(value); + } + catch (Exception ex) + { + throw new BiDiException($"Could not retrieve property {property.Name} from {property.DeclaringType}", ex); + } + + values.Add([property.Name, ConvertFrom(propertyValue)]); + } + + return new ObjectLocalValue(values); } } diff --git a/dotnet/src/webdriver/BiDi/Modules/Script/RegExpValue.cs b/dotnet/src/webdriver/BiDi/Modules/Script/RegExpValue.cs index 47a2239ab093d..eb25c091e8f91 100644 --- a/dotnet/src/webdriver/BiDi/Modules/Script/RegExpValue.cs +++ b/dotnet/src/webdriver/BiDi/Modules/Script/RegExpValue.cs @@ -17,9 +17,62 @@ // under the License. // +using System; +using System.Diagnostics; +using System.Text.RegularExpressions; + namespace OpenQA.Selenium.BiDi.Modules.Script; public record RegExpValue(string Pattern) { public string? Flags { get; set; } + + internal static string? GetRegExpFlags(RegexOptions options) + { + if (options == RegexOptions.None) + { + return null; + } + + 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 flags; + } } diff --git a/dotnet/test/common/BiDi/Script/LocalValueConversionTests.cs b/dotnet/test/common/BiDi/Script/LocalValueConversionTests.cs index c0a1058a55ef5..0ef090b7a0bcd 100644 --- a/dotnet/test/common/BiDi/Script/LocalValueConversionTests.cs +++ b/dotnet/test/common/BiDi/Script/LocalValueConversionTests.cs @@ -19,6 +19,10 @@ using NUnit.Framework; using OpenQA.Selenium.BiDi.Modules.Script; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; namespace OpenQA.Selenium.BiDi.Script; @@ -28,72 +32,211 @@ class LocalValueConversionTests public void CanConvertNullBoolToLocalValue() { bool? arg = null; - LocalValue result = arg; - Assert.That(result, Is.TypeOf()); + + AssertValue(arg); + AssertValue(LocalValue.ConvertFrom(arg)); + + static void AssertValue(LocalValue value) + { + Assert.That(value, Is.TypeOf()); + } } [Test] public void CanConvertTrueToLocalValue() { - LocalValue result = true; - Assert.That(result, Is.TypeOf()); - Assert.That((result as BooleanLocalValue).Value, Is.True); + AssertValue(true); + + AssertValue(LocalValue.ConvertFrom(true)); + + static void AssertValue(LocalValue value) + { + Assert.That(value, Is.TypeOf()); + Assert.That((value as BooleanLocalValue).Value, Is.True); + } } [Test] public void CanConvertFalseToLocalValue() { - LocalValue result = false; - Assert.That(result, Is.TypeOf()); - Assert.That((result as BooleanLocalValue).Value, Is.False); + AssertValue(false); + + AssertValue(LocalValue.ConvertFrom(false)); + + static void AssertValue(LocalValue value) + { + Assert.That(value, Is.TypeOf()); + Assert.That((value as BooleanLocalValue).Value, Is.False); + } } [Test] public void CanConvertNullIntToLocalValue() { int? arg = null; - LocalValue result = arg; - Assert.That(result, Is.TypeOf()); + + AssertValue(arg); + + AssertValue(LocalValue.ConvertFrom(arg)); + + static void AssertValue(LocalValue value) + { + Assert.That(value, Is.TypeOf()); + } } [Test] public void CanConvertZeroIntToLocalValue() { - LocalValue result = 0; - Assert.That(result, Is.TypeOf()); - Assert.That((result as NumberLocalValue).Value, Is.Zero); + int arg = 0; + + AssertValue(arg); + + AssertValue(LocalValue.ConvertFrom(arg)); + + static void AssertValue(LocalValue value) + { + Assert.That(value, Is.TypeOf()); + Assert.That((value as NumberLocalValue).Value, Is.Zero); + } } [Test] public void CanConvertNullDoubleToLocalValue() { double? arg = null; - LocalValue result = arg; - Assert.That(result, Is.TypeOf()); + + AssertValue(arg); + + AssertValue(LocalValue.ConvertFrom(arg)); + + static void AssertValue(LocalValue value) + { + Assert.That(value, Is.TypeOf()); + } } [Test] public void CanConvertZeroDoubleToLocalValue() { double arg = 0; - LocalValue result = arg; - Assert.That(result, Is.TypeOf()); - Assert.That((result as NumberLocalValue).Value, Is.Zero); + + AssertValue(arg); + + AssertValue(LocalValue.ConvertFrom(arg)); + + static void AssertValue(LocalValue value) + { + Assert.That(value, Is.TypeOf()); + Assert.That((value as NumberLocalValue).Value, Is.Zero); + } } [Test] public void CanConvertNullStringToLocalValue() { string arg = null; - LocalValue result = arg; - Assert.That(result, Is.TypeOf()); + + AssertValue(arg); + + AssertValue(LocalValue.ConvertFrom(arg)); + + static void AssertValue(LocalValue value) + { + Assert.That(value, Is.TypeOf()); + } } [Test] public void CanConvertStringToLocalValue() { - LocalValue result = "value"; - Assert.That(result, Is.TypeOf()); - Assert.That((result as StringLocalValue).Value, Is.EqualTo("value")); + AssertValue("value"); + + AssertValue(LocalValue.ConvertFrom("value")); + + static void AssertValue(LocalValue value) + { + Assert.That(value, Is.TypeOf()); + Assert.That((value as StringLocalValue).Value, Is.EqualTo("value")); + } + } + + [Test] + public void CanConvertDateTimeOffsetToLocalValue() + { + var date = new DateTimeOffset(2025, 4, 13, 5, 40, 20, 123, 456, TimeSpan.FromHours(+3)); + + AssertValue(date); + + AssertValue(LocalValue.ConvertFrom(date)); + + static void AssertValue(LocalValue value) + { + Assert.That(value, Is.TypeOf()); + Assert.That((value as DateLocalValue).Value, Is.EqualTo("2025-04-13T05:40:20.1234560+03:00")); + } + } + + [Test] + public void CanConvertArrayToLocalValue() + { + AssertValue(LocalValue.ConvertFrom(new List { 1, 2 })); + + AssertValue(LocalValue.ConvertFrom(new string[] { "a", "b" })); + + static void AssertValue(LocalValue value) + { + Assert.That(value, Is.TypeOf()); + Assert.That((value as ArrayLocalValue).Value.Count, Is.EqualTo(2)); + } + } + + [Test] + public void CanConvertMapToLocalValue() + { + AssertValue(LocalValue.ConvertFrom(new Dictionary { { 1, "a" }, { 2, "b" } })); + + static void AssertValue(LocalValue value) + { + Assert.That(value, Is.TypeOf()); + Assert.That((value as MapLocalValue).Value.Count, Is.EqualTo(2)); + } + } + + [Test] + public void CanConvertSetToLocalValue() + { + AssertValue(LocalValue.ConvertFrom(new HashSet { 1, 2 })); + + AssertValue(LocalValue.ConvertFrom(ImmutableHashSet.CreateRange([1, 2]))); + + static void AssertValue(LocalValue value) + { + Assert.That(value, Is.TypeOf()); + Assert.That((value as SetLocalValue).Value.Count, Is.EqualTo(2)); + } + } + + [Test] + public void CanConvertObjectValue() + { + var arg = new + { + UIntNumber = 5u, + Array = new int[] { 1, 2 }, + List = new List { "a", "b" }, + Dictionary = new Dictionary { { "a", 1 }, { "b", 2 } }, + Set = new HashSet { "a", "b" } + }; + + var value = LocalValue.ConvertFrom(arg); + + Console.WriteLine(value); + + Assert.That(value, Is.TypeOf()); + + var objValue = value as ObjectLocalValue; + + Assert.That(objValue.Value, Has.Exactly(5).Count); } }