From 1a38cbf5046db6595d2fd9410bfd92c9f054c8e6 Mon Sep 17 00:00:00 2001 From: Michael Render Date: Fri, 13 Jun 2025 13:38:45 -0400 Subject: [PATCH 1/7] [dotnet] Handle negative zero BiDi response --- .../Json/Converters/BiDiDoubleConverter.cs | 92 +++++++++++++++++++ .../src/webdriver/BiDi/Script/LocalValue.cs | 3 +- .../src/webdriver/BiDi/Script/RemoteValue.cs | 3 +- .../BiDi/Script/CallFunctionLocalValueTest.cs | 2 - 4 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 dotnet/src/webdriver/BiDi/Communication/Json/Converters/BiDiDoubleConverter.cs diff --git a/dotnet/src/webdriver/BiDi/Communication/Json/Converters/BiDiDoubleConverter.cs b/dotnet/src/webdriver/BiDi/Communication/Json/Converters/BiDiDoubleConverter.cs new file mode 100644 index 0000000000000..4154d2ac74cca --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Communication/Json/Converters/BiDiDoubleConverter.cs @@ -0,0 +1,92 @@ +// +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace OpenQA.Selenium.BiDi.Communication.Json.Converters; + +/// +/// Serializes and deserializes into a +/// BiDi spec-compliant number value. +/// +internal sealed class BiDiDoubleConverter : JsonConverter +{ + public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TryGetDouble(out double d)) + { + return d; + } + + var str = reader.GetString() ?? throw new JsonException(); + + if (str.Equals("-0", StringComparison.Ordinal)) + { + return -0.0; + } + else if (str.Equals("NaN", StringComparison.Ordinal)) + { + return double.NaN; + } + else if (str.Equals("Infinity", StringComparison.Ordinal)) + { + return double.PositiveInfinity; + } + else if (str.Equals("-Infinity", StringComparison.Ordinal)) + { + return double.NegativeInfinity; + } + + throw new JsonException(); + } + + public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) + { + if (double.IsNaN(value)) + { + writer.WriteStringValue("NaN"); + } + else if (double.IsPositiveInfinity(value)) + { + writer.WriteStringValue("Infinity"); + } + else if (double.IsNegativeInfinity(value)) + { + writer.WriteStringValue("-Infinity"); + } + else if (IsNegativeZero(value)) + { + writer.WriteStringValue("-0"); + } + else + { + writer.WriteNumberValue(value); + } + + static bool IsNegativeZero(double x) + { + // Negative zero is less trivial to test, because 0 == -0 is true + // We need to do a bit pattern comparison + + return BitConverter.DoubleToInt64Bits(x) == BitConverter.DoubleToInt64Bits(-0.0); + } + } +} diff --git a/dotnet/src/webdriver/BiDi/Script/LocalValue.cs b/dotnet/src/webdriver/BiDi/Script/LocalValue.cs index 0409f4542b7df..ce37fca5c26d6 100644 --- a/dotnet/src/webdriver/BiDi/Script/LocalValue.cs +++ b/dotnet/src/webdriver/BiDi/Script/LocalValue.cs @@ -24,6 +24,7 @@ using System.Numerics; using System.Text.Json.Serialization; using System.Text.RegularExpressions; +using OpenQA.Selenium.BiDi.Communication.Json.Converters; namespace OpenQA.Selenium.BiDi.Script; @@ -282,7 +283,7 @@ private static LocalValue ReflectionBasedConvertFrom(object? value) public abstract record PrimitiveProtocolLocalValue : LocalValue; -public record NumberLocalValue(double Value) : PrimitiveProtocolLocalValue +public record NumberLocalValue([property: JsonConverter(typeof(BiDiDoubleConverter))] double Value) : PrimitiveProtocolLocalValue { public static explicit operator NumberLocalValue(double n) => new NumberLocalValue(n); } diff --git a/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs b/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs index d1ac8be390be0..1d66710fa563d 100644 --- a/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs +++ b/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs @@ -22,6 +22,7 @@ using System.Numerics; using System.Text.Json; using System.Text.Json.Serialization; +using OpenQA.Selenium.BiDi.Communication.Json.Converters; namespace OpenQA.Selenium.BiDi.Script; @@ -97,7 +98,7 @@ public abstract record RemoteValue } } -public record NumberRemoteValue(double Value) : PrimitiveProtocolRemoteValue; +public record NumberRemoteValue([property: JsonConverter(typeof(BiDiDoubleConverter))] double Value) : PrimitiveProtocolRemoteValue; public record BooleanRemoteValue(bool Value) : PrimitiveProtocolRemoteValue; diff --git a/dotnet/test/common/BiDi/Script/CallFunctionLocalValueTest.cs b/dotnet/test/common/BiDi/Script/CallFunctionLocalValueTest.cs index e265e7f4f5d8f..6a75d595c5034 100644 --- a/dotnet/test/common/BiDi/Script/CallFunctionLocalValueTest.cs +++ b/dotnet/test/common/BiDi/Script/CallFunctionLocalValueTest.cs @@ -221,8 +221,6 @@ public async Task CanCallFunctionWithArgumentNumberZero() } [Test] - [IgnoreBrowser(Selenium.Browser.Edge, "Chromium can't handle -0 argument as a number: https://github.com/w3c/webdriver-bidi/issues/887")] - [IgnoreBrowser(Selenium.Browser.Chrome, "Chromium can't handle -0 argument as a number: https://github.com/w3c/webdriver-bidi/issues/887")] public async Task CanCallFunctionWithArgumentNumberNegativeZero() { var arg = new NumberLocalValue(double.NegativeZero); From 2ff92c8ea1a1277d9221babe79fd536e1eb2a53a Mon Sep 17 00:00:00 2001 From: Michael Render Date: Sun, 5 Oct 2025 15:17:18 -0400 Subject: [PATCH 2/7] Rename `BiDiDoubleConverter` to `SpecialNumberConverter` --- .../{BiDiDoubleConverter.cs => SpecialNumberConverter.cs} | 6 +++--- dotnet/src/webdriver/BiDi/Script/LocalValue.cs | 2 +- dotnet/src/webdriver/BiDi/Script/RemoteValue.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename dotnet/src/webdriver/BiDi/Communication/Json/Converters/{BiDiDoubleConverter.cs => SpecialNumberConverter.cs} (94%) diff --git a/dotnet/src/webdriver/BiDi/Communication/Json/Converters/BiDiDoubleConverter.cs b/dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs similarity index 94% rename from dotnet/src/webdriver/BiDi/Communication/Json/Converters/BiDiDoubleConverter.cs rename to dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs index 4154d2ac74cca..a630f87d6e671 100644 --- a/dotnet/src/webdriver/BiDi/Communication/Json/Converters/BiDiDoubleConverter.cs +++ b/dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs @@ -1,4 +1,4 @@ -// +// // Licensed to the Software Freedom Conservancy (SFC) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information @@ -27,7 +27,7 @@ namespace OpenQA.Selenium.BiDi.Communication.Json.Converters; /// Serializes and deserializes into a /// BiDi spec-compliant number value. /// -internal sealed class BiDiDoubleConverter : JsonConverter +internal sealed class SpecialNumberConverter : JsonConverter { public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { @@ -36,7 +36,7 @@ public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS return d; } - var str = reader.GetString() ?? throw new JsonException(); + var str = reader.GetString() ?? throw new JsonException("Cannot convert from null to remote number value"); if (str.Equals("-0", StringComparison.Ordinal)) { diff --git a/dotnet/src/webdriver/BiDi/Script/LocalValue.cs b/dotnet/src/webdriver/BiDi/Script/LocalValue.cs index 9da42d1ee6f64..e3af6cace2fb4 100644 --- a/dotnet/src/webdriver/BiDi/Script/LocalValue.cs +++ b/dotnet/src/webdriver/BiDi/Script/LocalValue.cs @@ -283,7 +283,7 @@ private static LocalValue ReflectionBasedConvertFrom(object? value) public abstract record PrimitiveProtocolLocalValue : LocalValue; -public sealed record NumberLocalValue([property: JsonConverter(typeof(BiDiDoubleConverter))] double Value) : PrimitiveProtocolLocalValue +public sealed record NumberLocalValue([property: JsonConverter(typeof(SpecialNumberConverter))] double Value) : PrimitiveProtocolLocalValue { public static explicit operator NumberLocalValue(double n) => new NumberLocalValue(n); } diff --git a/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs b/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs index 16d62b3aafebb..fddf5e072c8da 100644 --- a/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs +++ b/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs @@ -99,7 +99,7 @@ public abstract record RemoteValue } } -public sealed record NumberRemoteValue([property: JsonConverter(typeof(BiDiDoubleConverter))] double Value) : PrimitiveProtocolRemoteValue; +public sealed record NumberRemoteValue([property: JsonConverter(typeof(SpecialNumberConverter))] double Value) : PrimitiveProtocolRemoteValue; public sealed record NumberRemoteValue(double Value) : PrimitiveProtocolRemoteValue; From 7f60880e76ecfa410927267fe9825d2e5d0ad091 Mon Sep 17 00:00:00 2001 From: Michael Render Date: Sun, 5 Oct 2025 15:46:26 -0400 Subject: [PATCH 3/7] Fix merge --- dotnet/src/webdriver/BiDi/Script/RemoteValue.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs b/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs index fddf5e072c8da..e42ad9b1a942a 100644 --- a/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs +++ b/dotnet/src/webdriver/BiDi/Script/RemoteValue.cs @@ -99,9 +99,9 @@ public abstract record RemoteValue } } -public sealed record NumberRemoteValue([property: JsonConverter(typeof(SpecialNumberConverter))] double Value) : PrimitiveProtocolRemoteValue; +public abstract record PrimitiveProtocolRemoteValue : RemoteValue; -public sealed record NumberRemoteValue(double Value) : PrimitiveProtocolRemoteValue; +public sealed record NumberRemoteValue([property: JsonConverter(typeof(SpecialNumberConverter))] double Value) : PrimitiveProtocolRemoteValue; public sealed record BooleanRemoteValue(bool Value) : PrimitiveProtocolRemoteValue; From 76a80dcc4c821d50f679eedb22814a0549f9d377 Mon Sep 17 00:00:00 2001 From: Michael Render Date: Sun, 5 Oct 2025 16:06:57 -0400 Subject: [PATCH 4/7] alter null check message --- .../Communication/Json/Converters/SpecialNumberConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs b/dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs index a630f87d6e671..12d307b3b7161 100644 --- a/dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs +++ b/dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs @@ -36,7 +36,7 @@ public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS return d; } - var str = reader.GetString() ?? throw new JsonException("Cannot convert from null to remote number value"); + var str = reader.GetString() ?? throw new JsonException("Cannot convert from null to special number value"); if (str.Equals("-0", StringComparison.Ordinal)) { From 02b6b3d01af46da547c86bfd87aef996d50ab0ef Mon Sep 17 00:00:00 2001 From: Michael Render Date: Sun, 5 Oct 2025 16:35:54 -0400 Subject: [PATCH 5/7] Fix special handling --- .../Json/Converters/SpecialNumberConverter.cs | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs b/dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs index 12d307b3b7161..60f044be88aee 100644 --- a/dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs +++ b/dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs @@ -31,31 +31,38 @@ internal sealed class SpecialNumberConverter : JsonConverter { public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TryGetDouble(out double d)) + switch (reader.TokenType) { - return d; - } + case JsonTokenType.Number: + return reader.GetDouble(); - var str = reader.GetString() ?? throw new JsonException("Cannot convert from null to special number value"); + case JsonTokenType.String: + var str = reader.GetString()!; + if (str.Equals("-0", StringComparison.Ordinal)) + { + return -0.0; + } - if (str.Equals("-0", StringComparison.Ordinal)) - { - return -0.0; - } - else if (str.Equals("NaN", StringComparison.Ordinal)) - { - return double.NaN; - } - else if (str.Equals("Infinity", StringComparison.Ordinal)) - { - return double.PositiveInfinity; - } - else if (str.Equals("-Infinity", StringComparison.Ordinal)) - { - return double.NegativeInfinity; - } + if (str.Equals("NaN", StringComparison.Ordinal)) + { + return double.NaN; + } + + if (str.Equals("Infinity", StringComparison.Ordinal)) + { + return double.PositiveInfinity; + } - throw new JsonException(); + if (str.Equals("-Infinity", StringComparison.Ordinal)) + { + return double.NegativeInfinity; + } + + throw new JsonException("JSON string could not be parsed to a special number"); + + default: + throw new JsonException($"JSON type not a number or string: {reader.TokenType}"); + } } public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) From 9b76121625b727cebe974427e869d51d1e964686 Mon Sep 17 00:00:00 2001 From: Michael Render Date: Mon, 6 Oct 2025 10:04:11 -0400 Subject: [PATCH 6/7] Work around AI limitations --- .../Communication/Json/Converters/SpecialNumberConverter.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs b/dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs index 60f044be88aee..7df129c99298b 100644 --- a/dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs +++ b/dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs @@ -23,10 +23,8 @@ namespace OpenQA.Selenium.BiDi.Communication.Json.Converters; -/// -/// Serializes and deserializes into a -/// BiDi spec-compliant number value. -/// +// Serializes and deserializes double into a BiDi spec-compliant number value. +// See https://w3c.github.io/webdriver-bidi/#type-script-PrimitiveProtocolValue internal sealed class SpecialNumberConverter : JsonConverter { public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) From c19dfcd292e0a6a5864cf50276118beefd18ff15 Mon Sep 17 00:00:00 2001 From: Michael Render Date: Mon, 6 Oct 2025 11:38:00 -0400 Subject: [PATCH 7/7] Update dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs Co-authored-by: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> --- .../Communication/Json/Converters/SpecialNumberConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs b/dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs index 7df129c99298b..5300c99028ea9 100644 --- a/dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs +++ b/dotnet/src/webdriver/BiDi/Communication/Json/Converters/SpecialNumberConverter.cs @@ -56,7 +56,7 @@ public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS return double.NegativeInfinity; } - throw new JsonException("JSON string could not be parsed to a special number"); + throw new JsonException($"JSON '{str}' string could not be parsed to a special number"); default: throw new JsonException($"JSON type not a number or string: {reader.TokenType}");