diff --git a/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.local.json b/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.local.json index 02df38a38..345fdeacd 100644 --- a/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.local.json +++ b/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.local.json @@ -1747,21 +1747,6 @@ "FAIL" ] }, - { - "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", - "testIdPattern": "[DeviceRequestPrompt.test.ts] *", - "platforms": [ - "darwin", - "linux", - "win32" - ], - "parameters": [ - "webDriverBiDi" - ], - "expectations": [ - "FAIL" - ] - }, { "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", "testIdPattern": "[launcher.spec] PuppeteerSharp *", @@ -1806,20 +1791,5 @@ "expectations": [ "FAIL" ] - }, - { - "comment": "This is part of organizing the webdriver bidi implementation, We will remove it one by one", - "testIdPattern": "[prerender.spec] *", - "platforms": [ - "darwin", - "linux", - "win32" - ], - "parameters": [ - "webDriverBiDi" - ], - "expectations": [ - "FAIL" - ] } ] diff --git a/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.upstream.json b/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.upstream.json index 1071fff35..d75627429 100644 --- a/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.upstream.json +++ b/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.upstream.json @@ -3468,11 +3468,20 @@ "comment": "TODO: add a comment explaining why this expectation is required (include links to issues)" }, { - "testIdPattern": "[extensions.spec] extensions service_worker target type should be available", - "platforms": ["darwin", "linux", "win32"], - "parameters": ["cdp", "chrome", "chrome-headless-shell"], - "expectations": ["SKIP"], + "platforms": [ + "darwin", + "linux", + "win32" + ], + "parameters": [ + "cdp", + "chrome", + "chrome-headless-shell" + ], + "expectations": [ + "SKIP" + ], "comment": "TODO: add a comment explaining why this expectation is required (include links to issues)" }, { diff --git a/lib/PuppeteerSharp.Tests/PrerenderTests/PrerenderTests.cs b/lib/PuppeteerSharp.Tests/PrerenderTests/PrerenderTests.cs index 09e452d19..cc998e530 100644 --- a/lib/PuppeteerSharp.Tests/PrerenderTests/PrerenderTests.cs +++ b/lib/PuppeteerSharp.Tests/PrerenderTests/PrerenderTests.cs @@ -31,7 +31,6 @@ public async Task CanNavigateToAPrerenderedPageViaPuppeteer() var button = await Page.WaitForSelectorAsync("button"); await button.ClickAsync(); - await Page.GoToAsync(TestConstants.ServerUrl + "/prerender/target.html"); Assert.That(await Page.EvaluateExpressionAsync("document.body.innerText"), Is.EqualTo("target")); } diff --git a/lib/PuppeteerSharp/Bidi/BidiHttpRequest.cs b/lib/PuppeteerSharp/Bidi/BidiHttpRequest.cs index bdf443767..f50f62d8e 100644 --- a/lib/PuppeteerSharp/Bidi/BidiHttpRequest.cs +++ b/lib/PuppeteerSharp/Bidi/BidiHttpRequest.cs @@ -39,7 +39,7 @@ private BidiHttpRequest(Request request, BidiFrame frame, BidiHttpRequest redire { _request = request; Frame = frame; - RedirectChainList = new List(); + RedirectChainList = []; Requests.AddItem(request, this); } diff --git a/lib/PuppeteerSharp/Bidi/BidiMouse.cs b/lib/PuppeteerSharp/Bidi/BidiMouse.cs new file mode 100644 index 000000000..ec178441d --- /dev/null +++ b/lib/PuppeteerSharp/Bidi/BidiMouse.cs @@ -0,0 +1,106 @@ +// * MIT License +// * +// * Copyright (c) DarĂ­o Kondratiuk +// * +// * Permission is hereby granted, free of charge, to any person obtaining a copy +// * of this software and associated documentation files (the "Software"), to deal +// * in the Software without restriction, including without limitation the rights +// * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// * copies of the Software, and to permit persons to whom the Software is +// * furnished to do so, subject to the following conditions: +// * +// * The above copyright notice and this permission notice shall be included in all +// * copies or substantial portions of the Software. +// * +// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// * SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PuppeteerSharp.Input; +using WebDriverBiDi.Input; + +namespace PuppeteerSharp.Bidi; + +internal class BidiMouse(BidiPage page) : Mouse +{ + public override Task DropAsync(decimal x, decimal y, DragData data) => throw new NotImplementedException(); + + public override Task DragAndDropAsync(decimal startX, decimal startY, decimal endX, decimal endY, int delay = 0) => throw new NotImplementedException(); + + public override Task ResetAsync() => throw new NotImplementedException(); + + public override Task MoveAsync(decimal x, decimal y, MoveOptions options = null) => throw new NotImplementedException(); + + public override Task UpAsync(ClickOptions options = null) => throw new NotImplementedException(); + + public override Task WheelAsync(decimal deltaX, decimal deltaY) => throw new NotImplementedException(); + + public override Task DragAsync(decimal startX, decimal startY, decimal endX, decimal endY) => throw new NotImplementedException(); + + public override Task DragEnterAsync(decimal x, decimal y, DragData data) => throw new NotImplementedException(); + + public override Task DragOverAsync(decimal x, decimal y, DragData data) => throw new NotImplementedException(); + + public override Task DownAsync(ClickOptions options = null) => throw new NotImplementedException(); + + public override async Task ClickAsync(decimal x, decimal y, ClickOptions options = null) + { + var actions = new List + { + new PointerMoveAction() + { + X = (long)Math.Round(x), + Y = (long)Math.Round(y), + }, + }; + + var pointerDownAction = new PointerDownAction(GetBidiButton(options?.Button ?? MouseButton.Left)); + var pointerUpAction = new PointerUpAction(GetBidiButton(options?.Button ?? MouseButton.Left)); + + for (var i = 1; i < (options?.Count ?? 1); ++i) + { + actions.Add(pointerDownAction); + actions.Add(pointerUpAction); + } + + actions.Add(pointerDownAction); + + if (options?.Delay is > 0) + { + actions.Add(new PauseAction() + { + Duration = TimeSpan.FromMilliseconds(options.Delay), + }); + } + + actions.Add(pointerUpAction); + + var finalSource = new PointerSourceActions(); + finalSource.Actions.AddRange(actions); + + await page.BidiMainFrame.BrowsingContext.PerformActionsAsync([finalSource]).ConfigureAwait(false); + } + + protected override void Dispose(bool disposing) => throw new NotImplementedException(); + + private long GetBidiButton(MouseButton optionsButton) + { + return optionsButton switch + { + MouseButton.Left => 0, + MouseButton.Middle => 1, + MouseButton.Right => 2, + MouseButton.Back => 3, + MouseButton.Forward => 4, + _ => throw new ArgumentOutOfRangeException(nameof(optionsButton), $"Unsupported mouse button: {optionsButton}"), + }; + } +} + diff --git a/lib/PuppeteerSharp/Bidi/BidiPage.cs b/lib/PuppeteerSharp/Bidi/BidiPage.cs index 3a8f34f4e..a09b2a806 100644 --- a/lib/PuppeteerSharp/Bidi/BidiPage.cs +++ b/lib/PuppeteerSharp/Bidi/BidiPage.cs @@ -44,6 +44,7 @@ internal BidiPage(BidiBrowserContext browserContext, BrowsingContext browsingCon Browser = browserContext.Browser; BidiMainFrame = BidiFrame.From(this, null, browsingContext); _cdpEmulationManager = new CdpEmulationManager(BidiMainFrame.Client); + Mouse = new BidiMouse(this); } /// diff --git a/lib/PuppeteerSharp/Bidi/BidiRealm.cs b/lib/PuppeteerSharp/Bidi/BidiRealm.cs index 569638bcb..755bcb8b1 100644 --- a/lib/PuppeteerSharp/Bidi/BidiRealm.cs +++ b/lib/PuppeteerSharp/Bidi/BidiRealm.cs @@ -245,6 +245,16 @@ private T DeserializeResult(object result) return (T)(object)Convert.ToBoolean(result, CultureInfo.InvariantCulture); } + if (typeof(T) == typeof(decimal)) + { + return (T)(object)Convert.ToDecimal(result, CultureInfo.InvariantCulture); + } + + if (result is RemoteValueDictionary remoteValueDictionary) + { + return DeserializeRemoteValueDictionary(remoteValueDictionary); + } + if (typeof(T).IsArray) { // Get the element type of the array @@ -282,6 +292,45 @@ private T DeserializeResult(object result) return (T)result; } + private T DeserializeRemoteValueDictionary(RemoteValueDictionary remoteValueDictionary) + { + // Create an instance of T + var instance = Activator.CreateInstance(); + var type = typeof(T); + + // Iterate through the dictionary and populate properties + foreach (var entry in remoteValueDictionary) + { + var propertyName = entry.Key?.ToString(); + if (string.IsNullOrEmpty(propertyName)) + { + continue; + } + + var remoteValue = entry.Value; + + // Find the property on the type (case-insensitive) + var property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + + if (property != null && property.CanWrite) + { + // Get the value from RemoteValue + var value = remoteValue.Value; + + // Recursively deserialize the value to the property type + var deserializedValue = typeof(BidiRealm) + .GetMethod(nameof(DeserializeResult), BindingFlags.Instance | BindingFlags.NonPublic) + ?.MakeGenericMethod(property.PropertyType) + .Invoke(this, [value]); + + // Set the property value + property.SetValue(instance, deserializedValue); + } + } + + return instance; + } + private async Task FormatArgumentAsync(object arg) { if (arg is TaskCompletionSource tcs) @@ -307,6 +356,9 @@ private async Task FormatArgumentAsync(object arg) case int integer when integer == -0: return LocalValue.NegativeZero; + case decimal decimalValue: + return LocalValue.Number(decimalValue); + case double d: if (double.IsPositiveInfinity(d)) { diff --git a/lib/PuppeteerSharp/Bidi/Core/BrowsingContext.cs b/lib/PuppeteerSharp/Bidi/Core/BrowsingContext.cs index f2a926245..178fbd139 100644 --- a/lib/PuppeteerSharp/Bidi/Core/BrowsingContext.cs +++ b/lib/PuppeteerSharp/Bidi/Core/BrowsingContext.cs @@ -26,6 +26,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Options; using WebDriverBiDi.BrowsingContext; +using WebDriverBiDi.Input; namespace PuppeteerSharp.Bidi.Core; @@ -162,6 +163,13 @@ internal async Task SetViewportAsync(SetViewportOptions options = null) await Session.Driver.BrowsingContext.SetViewportAsync(parameters).ConfigureAwait(false); } + internal async Task PerformActionsAsync(SourceActions[] actions) + { + var param = new PerformActionsCommandParameters(Id); + param.Actions.AddRange(actions); + await Session.Driver.Input.PerformActionsAsync(param).ConfigureAwait(false); + } + internal WindowRealm CreateWindowRealm(string sandbox = null) { var realm = WindowRealm.From(this, sandbox); diff --git a/lib/PuppeteerSharp/Bidi/Core/Realm.cs b/lib/PuppeteerSharp/Bidi/Core/Realm.cs index 788a602f4..f5cd20323 100644 --- a/lib/PuppeteerSharp/Bidi/Core/Realm.cs +++ b/lib/PuppeteerSharp/Bidi/Core/Realm.cs @@ -21,6 +21,7 @@ // * SOFTWARE. using System; +using System.Linq; using System.Threading.Tasks; using WebDriverBiDi.Script; diff --git a/lib/PuppeteerSharp/ElementHandle.cs b/lib/PuppeteerSharp/ElementHandle.cs index b9463b420..c428712f7 100644 --- a/lib/PuppeteerSharp/ElementHandle.cs +++ b/lib/PuppeteerSharp/ElementHandle.cs @@ -30,7 +30,7 @@ internal ElementHandle(JSHandle handle) /// /// Base handle. /// - protected JSHandle Handle { get; } + internal JSHandle Handle { get; } /// /// Element's page. @@ -451,8 +451,9 @@ public Task IsIntersectingViewportAsync(decimal threshold) => BindIsolatedHandleAsync(async handle => { await handle.AssertConnectedElementAsync().ConfigureAwait(false); - var svgHandle = await AsSVGElementHandleAsync(this).ConfigureAwait(false); - var target = svgHandle == null ? handle : await svgHandle.GetOwnerSVGElementAsync().ConfigureAwait(false); + var svgHandle = await AsSVGElementHandleAsync(handle).ConfigureAwait(false); + var ownerSVGElement = svgHandle == null ? null : await svgHandle.GetOwnerSVGElementAsync().ConfigureAwait(false); + var target = ownerSVGElement ?? handle; return await target.Realm.EvaluateFunctionAsync( @"async (element, threshold) => { @@ -662,18 +663,6 @@ public override Task> GetPropertiesAsync() public override Task JsonValueAsync() => BindIsolatedHandleAsync(element => element.Handle.JsonValueAsync()); - /// - public virtual Task ScrollIntoViewAsync() - => BindIsolatedHandleAsync(handle - => handle.EvaluateFunctionAsync( - @"element => { - element.scrollIntoView({ - block: 'center', - inline: 'center', - behavior: 'instant', - }); - }")); - /// public override async ValueTask DisposeAsync() { @@ -688,6 +677,18 @@ public override async ValueTask DisposeAsync() GC.SuppressFinalize(this); } + /// + public virtual Task ScrollIntoViewAsync() + => BindIsolatedHandleAsync(handle + => handle.EvaluateFunctionAsync( + @"element => { + element.scrollIntoView({ + block: 'center', + inline: 'center', + behavior: 'instant', + }); + }")); + /// /// Checks whether the element is still connected to the browser. /// diff --git a/lib/PuppeteerSharp/Helpers/DictionaryExtensions.cs b/lib/PuppeteerSharp/Helpers/DictionaryExtensions.cs index 9ba649e83..3ce2b8f47 100644 --- a/lib/PuppeteerSharp/Helpers/DictionaryExtensions.cs +++ b/lib/PuppeteerSharp/Helpers/DictionaryExtensions.cs @@ -1,4 +1,8 @@ +using System; +using System.Collections; using System.Collections.Generic; +using System.Text.Json; +using PuppeteerSharp.Helpers.Json; namespace PuppeteerSharp.Helpers {