diff --git a/dotnet/src/webdriver/BiDi/BrowsingContext/SetViewportCommand.cs b/dotnet/src/webdriver/BiDi/BrowsingContext/SetViewportCommand.cs index 90c493a0de6d5..9021c64551106 100644 --- a/dotnet/src/webdriver/BiDi/BrowsingContext/SetViewportCommand.cs +++ b/dotnet/src/webdriver/BiDi/BrowsingContext/SetViewportCommand.cs @@ -17,16 +17,19 @@ // under the License. // +using OpenQA.Selenium.BiDi.Json.Converters; +using System.Text.Json.Serialization; + namespace OpenQA.Selenium.BiDi.BrowsingContext; internal sealed class SetViewportCommand(SetViewportParameters @params) : Command(@params, "browsingContext.setViewport"); -internal sealed record SetViewportParameters(BrowsingContext Context, Viewport? Viewport, double? DevicePixelRatio) : Parameters; +internal sealed record SetViewportParameters(BrowsingContext Context, [property: JsonConverter(typeof(OptionalConverter))] Optional? Viewport, double? DevicePixelRatio) : Parameters; public sealed class SetViewportOptions : CommandOptions { - public Viewport? Viewport { get; set; } + public Optional? Viewport { get; set; } public double? DevicePixelRatio { get; set; } } diff --git a/dotnet/src/webdriver/BiDi/Json/Converters/OptionalConverter.cs b/dotnet/src/webdriver/BiDi/Json/Converters/OptionalConverter.cs new file mode 100644 index 0000000000000..0fa368adce1f9 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Json/Converters/OptionalConverter.cs @@ -0,0 +1,51 @@ +// +// 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.Json.Converters; + +public sealed class OptionalConverter : JsonConverter> +{ + public override Optional Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + reader.Read(); // consume null + return new Optional(default!); + } + + T value = JsonSerializer.Deserialize(ref reader, options)!; + return new Optional(value); + } + + public override void Write(Utf8JsonWriter writer, Optional value, JsonSerializerOptions options) + { + if (value.TryGetValue(out var optionalValue)) + { + JsonSerializer.Serialize(writer, optionalValue, options); + } + else + { + writer.WriteNullValue(); + } + } +} diff --git a/dotnet/src/webdriver/BiDi/Optional.cs b/dotnet/src/webdriver/BiDi/Optional.cs new file mode 100644 index 0000000000000..e31fbbce2c9c6 --- /dev/null +++ b/dotnet/src/webdriver/BiDi/Optional.cs @@ -0,0 +1,47 @@ +// +// 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; + +namespace OpenQA.Selenium.BiDi; + +public readonly record struct Optional +{ + private readonly T _value; + public bool HasValue { get; } + + public T Value => HasValue + ? _value + : throw new InvalidOperationException("Optional has no value. Check IsSet first."); + + public Optional(T value) + { + _value = value; + HasValue = true; + } + + public bool TryGetValue(out T value) + { + value = _value; + return HasValue; + } + + // implicit conversion from T -> Optional + public static implicit operator Optional(T value) => new(value); +} diff --git a/dotnet/test/common/BiDi/BrowsingContext/BrowsingContextTest.cs b/dotnet/test/common/BiDi/BrowsingContext/BrowsingContextTest.cs index a1a8cda5924b8..9a62004bede8b 100644 --- a/dotnet/test/common/BiDi/BrowsingContext/BrowsingContextTest.cs +++ b/dotnet/test/common/BiDi/BrowsingContext/BrowsingContextTest.cs @@ -298,13 +298,40 @@ public async Task CanCaptureScreenshotOfElement() [Test] public async Task CanSetViewport() { - await context.SetViewportAsync(new() { Viewport = new(250, 300) }); + Task GetWidthAsync() => context.Script.EvaluateAsync("window.innerWidth", false); + Task GetHeightAsync() => context.Script.EvaluateAsync("window.innerHeight", false); + + var defaultWidth = await GetWidthAsync(); + var defaultHeight = await GetHeightAsync(); + + await context.SetViewportAsync(new() { Viewport = new Viewport(250, 300) }); + + Assert.That(await GetWidthAsync(), Is.EqualTo(250)); + Assert.That(await GetHeightAsync(), Is.EqualTo(300)); + + await context.SetViewportAsync(new() { Viewport = new Viewport(250, 300) }); + await context.SetViewportAsync(); // Sends nothing + + Assert.That(await GetWidthAsync(), Is.EqualTo(250)); + Assert.That(await GetHeightAsync(), Is.EqualTo(300)); + + await context.SetViewportAsync(new() { Viewport = new Viewport(250, 300) }); + await context.SetViewportAsync(new() { Viewport = default }); // Sends nothing + + Assert.That(await GetWidthAsync(), Is.EqualTo(250)); + Assert.That(await GetHeightAsync(), Is.EqualTo(300)); + + await context.SetViewportAsync(new() { Viewport = new Viewport(250, 300) }); + await context.SetViewportAsync(new() { Viewport = default(Viewport?) }); // Explicitly sends "null", resetting to default + + Assert.That(await GetWidthAsync(), Is.EqualTo(defaultWidth)); + Assert.That(await GetHeightAsync(), Is.EqualTo(defaultHeight)); } [Test] public async Task CanSetViewportWithDevicePixelRatio() { - await context.SetViewportAsync(new() { Viewport = new(250, 300), DevicePixelRatio = 5 }); + await context.SetViewportAsync(new() { Viewport = new Viewport(250, 300), DevicePixelRatio = 5 }); } [Test]