diff --git a/CLAUDE.md b/CLAUDE.md index 5d8367710..0336bf661 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -389,7 +389,7 @@ Test directory structure demonstrates comprehensive coverage: **IMPORTANT: Test Expectations Files Rules:** - `TestExpectations.upstream.json`: This file should NEVER be edited unless syncing with the upstream Puppeteer project. It contains expectations that match the upstream Puppeteer test expectations. -- `TestExpectations.local.json`: Use this file for local overrides and PuppeteerSharp-specific test expectations. Add entries here to skip or mark tests that fail due to .NET-specific issues or features not yet implemented. +- `TestExpectations.local.json`: Use this file for local overrides and PuppeteerSharp-specific test expectations. Add entries here to skip or mark tests that fail due to .NET-specific issues or features not yet implemented. Never add entries to this file that are meant to match upstream expectations and never add entries without explicit confirmation. #### Test Server (`PuppeteerSharp.TestServer/`) - ASP.NET Core server for hosting test pages diff --git a/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.upstream.json b/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.upstream.json index dc41fcca8..1fcc7af10 100644 --- a/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.upstream.json +++ b/lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.upstream.json @@ -167,6 +167,13 @@ "expectations": ["SKIP"], "comment": "TODO: add a comment explaining why this expectation is required (include links to issues)" }, + { + "testIdPattern": "[page.spec] Page Page.resize should resize the browser window to fit page content", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"], + "comment": "Not supported by WebDriver BiDi" + }, { "testIdPattern": "[pipe.spec] *", "platforms": ["darwin", "linux", "win32"], diff --git a/lib/PuppeteerSharp.TestServer/wwwroot/style-404.html b/lib/PuppeteerSharp.TestServer/wwwroot/style-404.html new file mode 100644 index 000000000..9b4e804f3 --- /dev/null +++ b/lib/PuppeteerSharp.TestServer/wwwroot/style-404.html @@ -0,0 +1,2 @@ + + diff --git a/lib/PuppeteerSharp.Tests/AccessibilityTests/AccessibilityTests.cs b/lib/PuppeteerSharp.Tests/AccessibilityTests/AccessibilityTests.cs index 6e7225996..8149d69b6 100644 --- a/lib/PuppeteerSharp.Tests/AccessibilityTests/AccessibilityTests.cs +++ b/lib/PuppeteerSharp.Tests/AccessibilityTests/AccessibilityTests.cs @@ -113,42 +113,22 @@ public async Task ShouldReportUninterestingNodes() await Page.SetContentAsync(""); await Page.FocusAsync("textarea"); - // This object has more children than in upstream. - // Because upstream uses `toMatchObject` which stops going deeper if the element has not Children. - Assert.That( - FindFocusedNode(await Page.Accessibility.SnapshotAsync(new AccessibilitySnapshotOptions - { - InterestingOnly = false - })), - Is.EqualTo(new SerializedAXNode - { - Role = "textbox", - Name = "", - Value = "hi", - Focused = true, - Multiline = true, - Children = new SerializedAXNode[] - { - new() { - Role = "generic", - Name = "", - Children = new SerializedAXNode[] - { - new() { - Role = "StaticText", - Name = "hi", - Children = new SerializedAXNode[] - { - new() - { - Role = "InlineTextBox", - } - } - } - } - } - } - })); + // Upstream uses toMatchObject (partial matching), so we assert individual properties + var focusedNode = FindFocusedNode(await Page.Accessibility.SnapshotAsync(new AccessibilitySnapshotOptions + { + InterestingOnly = false + })); + Assert.That(focusedNode.Role, Is.EqualTo("textbox")); + Assert.That(focusedNode.Name, Is.EqualTo("")); + Assert.That(focusedNode.Value, Is.EqualTo("hi")); + Assert.That(focusedNode.Focused, Is.True); + Assert.That(focusedNode.Multiline, Is.True); + Assert.That(focusedNode.Children, Has.Length.EqualTo(1)); + Assert.That(focusedNode.Children[0].Role, Is.EqualTo("generic")); + Assert.That(focusedNode.Children[0].Name, Is.EqualTo("")); + Assert.That(focusedNode.Children[0].Children, Has.Length.EqualTo(1)); + Assert.That(focusedNode.Children[0].Children[0].Role, Is.EqualTo("StaticText")); + Assert.That(focusedNode.Children[0].Children[0].Name, Is.EqualTo("hi")); } [Test, PuppeteerTest("accessibility.spec", "Accessibility", "get snapshots while the tree is re-calculated")] diff --git a/lib/PuppeteerSharp.Tests/AccessibilityTests/RootOptionTests.cs b/lib/PuppeteerSharp.Tests/AccessibilityTests/RootOptionTests.cs index 848bb6df9..e6e3368eb 100644 --- a/lib/PuppeteerSharp.Tests/AccessibilityTests/RootOptionTests.cs +++ b/lib/PuppeteerSharp.Tests/AccessibilityTests/RootOptionTests.cs @@ -91,46 +91,34 @@ public async Task ShouldReturnNullWhenTheElementIsNoLongerInDOM() [Test, PuppeteerTest("accessibility.spec", "root option", "should support the interestingOnly option")] public async Task ShouldSupportTheInterestingOnlyOption() { - await Page.SetContentAsync("
"); - var div = await Page.QuerySelectorAsync("div"); + await Page.SetContentAsync("
"); + var div = await Page.QuerySelectorAsync("div.uninteresting"); Assert.That(await Page.Accessibility.SnapshotAsync(new AccessibilitySnapshotOptions { Root = div }), Is.Null); - Assert.That( - await Page.Accessibility.SnapshotAsync(new AccessibilitySnapshotOptions - { - Root = div, - InterestingOnly = false - }), - Is.EqualTo(new SerializedAXNode - { - Role = "generic", - Name = "", - Children = new[] - { - new SerializedAXNode - { - Role = "button", - Name = "My Button", - Children = new[] - { - new SerializedAXNode - { - Role = "StaticText", - Name = "My Button", - Children = new SerializedAXNode[] - { - new() - { - Role = "InlineTextBox", - } - } - } - } - } - } - })); + + var divWithButton = await Page.QuerySelectorAsync("div"); + var snapshot = await Page.Accessibility.SnapshotAsync(new AccessibilitySnapshotOptions + { + Root = divWithButton + }); + Assert.That(snapshot.Name, Is.EqualTo("My Button")); + Assert.That(snapshot.Role, Is.EqualTo("button")); + + var fullSnapshot = await Page.Accessibility.SnapshotAsync(new AccessibilitySnapshotOptions + { + Root = divWithButton, + InterestingOnly = false + }); + Assert.That(fullSnapshot.Role, Is.EqualTo("generic")); + Assert.That(fullSnapshot.Name, Is.EqualTo("")); + Assert.That(fullSnapshot.Children, Has.Length.EqualTo(1)); + Assert.That(fullSnapshot.Children[0].Role, Is.EqualTo("button")); + Assert.That(fullSnapshot.Children[0].Name, Is.EqualTo("My Button")); + Assert.That(fullSnapshot.Children[0].Children, Has.Length.EqualTo(1)); + Assert.That(fullSnapshot.Children[0].Children[0].Role, Is.EqualTo("StaticText")); + Assert.That(fullSnapshot.Children[0].Children[0].Name, Is.EqualTo("My Button")); } } } diff --git a/lib/PuppeteerSharp.Tests/OOPIFTests/OOPIFTests.cs b/lib/PuppeteerSharp.Tests/OOPIFTests/OOPIFTests.cs index 8cdf85222..274990f6b 100644 --- a/lib/PuppeteerSharp.Tests/OOPIFTests/OOPIFTests.cs +++ b/lib/PuppeteerSharp.Tests/OOPIFTests/OOPIFTests.cs @@ -239,7 +239,7 @@ await FrameUtils.AttachFrameAsync( "frame1", TestConstants.CrossProcessHttpPrefix + "/empty.html" ); - var frame = await frameTask.WithTimeout(); + var frame = await frameTask.WithTimeout(5_000); await frame.EvaluateFunctionAsync(@"() => { const button = document.createElement('button'); button.id = 'test-button'; diff --git a/lib/PuppeteerSharp.Tests/PageTests/PageEventsConsoleTests.cs b/lib/PuppeteerSharp.Tests/PageTests/PageEventsConsoleTests.cs index a3c184334..b6bea9160 100644 --- a/lib/PuppeteerSharp.Tests/PageTests/PageEventsConsoleTests.cs +++ b/lib/PuppeteerSharp.Tests/PageTests/PageEventsConsoleTests.cs @@ -186,18 +186,18 @@ await Page.EvaluateFunctionAsync(@"() => { [Test, PuppeteerTest("page.spec", "Page Page.Events.Console", "should trigger correct Log")] public async Task ShouldTriggerCorrectLog() { - // Navigate to about:blank first (different origin than the test server) - await Page.GoToAsync(TestConstants.AboutBlank); + // Navigate to localhost (one origin) + await Page.GoToAsync(TestConstants.EmptyPage); var messageTask = new TaskCompletionSource(); Page.Console += (_, e) => messageTask.TrySetResult(e.Message); - // Fetch from a different origin to trigger CORS error + // Fetch from 127.0.0.1 (different origin) to trigger CORS error await Task.WhenAll( messageTask.Task, Page.EvaluateFunctionAsync( "async url => await fetch(url).catch(() => {})", - TestConstants.EmptyPage)); + $"{TestConstants.CrossProcessUrl}/empty.html")); var message = await messageTask.Task; Assert.That(message.Text, Does.Contain("Access-Control-Allow-Origin")); diff --git a/lib/PuppeteerSharp.Tests/PageTests/ResizeTests.cs b/lib/PuppeteerSharp.Tests/PageTests/ResizeTests.cs new file mode 100644 index 000000000..2ea5f5982 --- /dev/null +++ b/lib/PuppeteerSharp.Tests/PageTests/ResizeTests.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using NUnit.Framework; +using PuppeteerSharp.Nunit; + +namespace PuppeteerSharp.Tests.PageTests +{ + public class ResizeTests : PuppeteerPageBaseTest + { + [Test, PuppeteerTest("page.spec", "Page Page.resize", "should resize the browser window to fit page content")] + public async Task ShouldResizeTheBrowserWindowToFitPageContent() + { + var options = TestConstants.DefaultBrowserOptions(); + options.DefaultViewport = null; + + await using var browser = await Puppeteer.LaunchAsync(options); + var page = await browser.NewPageAsync(); + + var contentWidth = 500; + var contentHeight = 400; + var resizedTask = page.EvaluateFunctionAsync( + "() => new Promise(resolve => { window.onresize = resolve; })"); + await page.ResizeAsync(contentWidth, contentHeight); + await resizedTask; + + var innerSize = await page.EvaluateFunctionAsync( + "() => ({ width: window.innerWidth, height: window.innerHeight })"); + Assert.That(innerSize.Width, Is.EqualTo(contentWidth)); + Assert.That(innerSize.Height, Is.EqualTo(contentHeight)); + } + + private sealed class Size + { + public int Width { get; set; } + + public int Height { get; set; } + } + } +} diff --git a/lib/PuppeteerSharp.Tests/RequestInterceptionExperimentalTests/PageSetRequestInterceptionTests.cs b/lib/PuppeteerSharp.Tests/RequestInterceptionExperimentalTests/PageSetRequestInterceptionTests.cs index ad22dbf2f..8570cb212 100644 --- a/lib/PuppeteerSharp.Tests/RequestInterceptionExperimentalTests/PageSetRequestInterceptionTests.cs +++ b/lib/PuppeteerSharp.Tests/RequestInterceptionExperimentalTests/PageSetRequestInterceptionTests.cs @@ -666,12 +666,15 @@ public async Task ShouldWorkWithEncodedServerNegative2() var requests = new List(); Page.AddRequestInterceptor(request => { - requests.Add(request); + if (!request.Url.Contains("favicon.ico")) + { + requests.Add(request); + } + return request.ContinueAsync(new Payload(), 0); }); var response = - await Page.GoToAsync( - $"data:text/html,"); + await Page.GoToAsync($"{TestConstants.ServerUrl}/style-404.html"); Assert.That(response.Status, Is.EqualTo(HttpStatusCode.OK)); Assert.That(requests, Has.Count.EqualTo(2)); Assert.That(requests[1].Response.Status, Is.EqualTo(HttpStatusCode.NotFound)); diff --git a/lib/PuppeteerSharp.Tests/RequestInterceptionTests/SetRequestInterceptionTests.cs b/lib/PuppeteerSharp.Tests/RequestInterceptionTests/SetRequestInterceptionTests.cs index 61340a4c6..f0584696e 100644 --- a/lib/PuppeteerSharp.Tests/RequestInterceptionTests/SetRequestInterceptionTests.cs +++ b/lib/PuppeteerSharp.Tests/RequestInterceptionTests/SetRequestInterceptionTests.cs @@ -541,10 +541,14 @@ public async Task ShouldWorkWithEncodedServerNegative2() var requests = new List(); Page.Request += async (_, e) => { - requests.Add(e.Request); + if (!e.Request.Url.Contains("favicon.ico")) + { + requests.Add(e.Request); + } + await e.Request.ContinueAsync(); }; - var response = await Page.GoToAsync($"data:text/html,"); + var response = await Page.GoToAsync($"{TestConstants.ServerUrl}/style-404.html"); Assert.That(response.Status, Is.EqualTo(HttpStatusCode.OK)); Assert.That(requests, Has.Count.EqualTo(2)); diff --git a/lib/PuppeteerSharp.Tests/TracingTests/TracingTests.cs b/lib/PuppeteerSharp.Tests/TracingTests/TracingTests.cs index 61169e326..7f1eff525 100644 --- a/lib/PuppeteerSharp.Tests/TracingTests/TracingTests.cs +++ b/lib/PuppeteerSharp.Tests/TracingTests/TracingTests.cs @@ -59,11 +59,11 @@ public async Task ShouldRunWithCustomCategoriesProvided() { await Page.Tracing.StartAsync(new TracingOptions { - Screenshots = true, Path = _file, Categories = new List { - "disabled-by-default-v8.cpu_profiler.hires" + "-*", + "disabled-by-default-devtools.timeline.frame", } }); @@ -73,11 +73,14 @@ await Page.Tracing.StartAsync(new TracingOptions using var document = JsonDocument.Parse(jsonString); var root = document.RootElement; - var metadata = root.GetProperty("metadata"); - var traceConfig = metadata.GetProperty("trace-config"); - - var traceConfigString = traceConfig.GetString(); - Assert.That(traceConfigString, Does.Contain("disabled-by-default-v8.cpu_profiler.hires")); + var traceEvents = root.GetProperty("traceEvents"); + foreach (var traceEvent in traceEvents.EnumerateArray()) + { + if (traceEvent.TryGetProperty("cat", out var cat)) + { + Assert.That(cat.GetString(), Is.Not.EqualTo("toplevel")); + } + } } [Test, PuppeteerTest("tracing.spec", "Tracing", "should run with default categories")] diff --git a/lib/PuppeteerSharp/Bidi/BidiPage.cs b/lib/PuppeteerSharp/Bidi/BidiPage.cs index 08741550a..19498ca27 100644 --- a/lib/PuppeteerSharp/Bidi/BidiPage.cs +++ b/lib/PuppeteerSharp/Bidi/BidiPage.cs @@ -226,6 +226,10 @@ public override async Task EmulateTimezoneAsync(string timezoneId) } } + /// + public override Task ResizeAsync(int contentWidth, int contentHeight) + => throw new NotSupportedException("Resize is not yet supported in WebDriver BiDi."); + /// public override Task EmulateIdleStateAsync(EmulateIdleOverrides idleOverrides = null) => _cdpEmulationManager.EmulateIdleStateAsync(idleOverrides); diff --git a/lib/PuppeteerSharp/BrowserData/Chrome.cs b/lib/PuppeteerSharp/BrowserData/Chrome.cs index fb45645d6..4383cc599 100644 --- a/lib/PuppeteerSharp/BrowserData/Chrome.cs +++ b/lib/PuppeteerSharp/BrowserData/Chrome.cs @@ -13,7 +13,7 @@ public static class Chrome /// /// Default chrome build. /// - public static string DefaultBuildId => "138.0.7204.101"; + public static string DefaultBuildId => "145.0.7632.46"; internal static async Task ResolveBuildIdAsync(ChromeReleaseChannel channel) => (await GetLastKnownGoodReleaseForChannel(channel).ConfigureAwait(false)).Version; diff --git a/lib/PuppeteerSharp/Cdp/CdpPage.cs b/lib/PuppeteerSharp/Cdp/CdpPage.cs index 4e5a37904..e3451926e 100644 --- a/lib/PuppeteerSharp/Cdp/CdpPage.cs +++ b/lib/PuppeteerSharp/Cdp/CdpPage.cs @@ -394,6 +394,22 @@ public override async Task SetViewportAsync(ViewPortOptions viewport) } } + /// + public override async Task ResizeAsync(int contentWidth, int contentHeight) + { + var response = await PrimaryTargetClient.SendAsync( + "Browser.getWindowForTarget").ConfigureAwait(false); + + await PrimaryTargetClient.SendAsync( + "Browser.setContentsSize", + new BrowserSetContentsSizeRequest + { + WindowId = response.WindowId, + Width = contentWidth, + Height = contentHeight, + }).ConfigureAwait(false); + } + /// public override Task EmulateNetworkConditionsAsync(NetworkConditions networkConditions) => FrameManager.NetworkManager.EmulateNetworkConditionsAsync(networkConditions); diff --git a/lib/PuppeteerSharp/Cdp/CdpWebWorker.cs b/lib/PuppeteerSharp/Cdp/CdpWebWorker.cs index 08de62b97..2a2416cb8 100644 --- a/lib/PuppeteerSharp/Cdp/CdpWebWorker.cs +++ b/lib/PuppeteerSharp/Cdp/CdpWebWorker.cs @@ -92,22 +92,35 @@ public override async Task CloseAsync() switch (_targetType) { case TargetType.ServiceWorker: + if (CdpCDPSession.Connection != null) + { + await CdpCDPSession.Connection.SendAsync( + "Target.closeTarget", + new TargetCloseTargetRequest() + { + TargetId = _id, + }).ConfigureAwait(false); + + await CdpCDPSession.Connection.SendAsync( + "Target.detachFromTarget", + new TargetDetachFromTargetRequest() + { + SessionId = Client.Id, + }).ConfigureAwait(false); + } + + break; case TargetType.SharedWorker: - // For service and shared workers we need to close the target and detach to allow - // the worker to stop. - await CdpCDPSession.Connection.SendAsync( - "Target.closeTarget", - new TargetCloseTargetRequest() - { - TargetId = _id, - }).ConfigureAwait(false); - - await CdpCDPSession.Connection.SendAsync( - "Target.detachFromTarget", - new TargetDetachFromTargetRequest() - { - SessionId = Client.Id, - }).ConfigureAwait(false); + if (CdpCDPSession.Connection != null) + { + await CdpCDPSession.Connection.SendAsync( + "Target.closeTarget", + new TargetCloseTargetRequest() + { + TargetId = _id, + }).ConfigureAwait(false); + } + break; default: await EvaluateFunctionAsync(@"() => { diff --git a/lib/PuppeteerSharp/Cdp/Messaging/BrowserGetWindowForTargetResponse.cs b/lib/PuppeteerSharp/Cdp/Messaging/BrowserGetWindowForTargetResponse.cs new file mode 100644 index 000000000..d71102dda --- /dev/null +++ b/lib/PuppeteerSharp/Cdp/Messaging/BrowserGetWindowForTargetResponse.cs @@ -0,0 +1,7 @@ +namespace PuppeteerSharp.Cdp.Messaging +{ + internal class BrowserGetWindowForTargetResponse + { + public int WindowId { get; set; } + } +} diff --git a/lib/PuppeteerSharp/Cdp/Messaging/BrowserSetContentsSizeRequest.cs b/lib/PuppeteerSharp/Cdp/Messaging/BrowserSetContentsSizeRequest.cs new file mode 100644 index 000000000..960aecbb9 --- /dev/null +++ b/lib/PuppeteerSharp/Cdp/Messaging/BrowserSetContentsSizeRequest.cs @@ -0,0 +1,11 @@ +namespace PuppeteerSharp.Cdp.Messaging +{ + internal class BrowserSetContentsSizeRequest + { + public int WindowId { get; set; } + + public int Width { get; set; } + + public int Height { get; set; } + } +} diff --git a/lib/PuppeteerSharp/Helpers/Json/SystemTextJsonSerializationContext.cs b/lib/PuppeteerSharp/Helpers/Json/SystemTextJsonSerializationContext.cs index e81e39bfe..a420df835 100644 --- a/lib/PuppeteerSharp/Helpers/Json/SystemTextJsonSerializationContext.cs +++ b/lib/PuppeteerSharp/Helpers/Json/SystemTextJsonSerializationContext.cs @@ -40,8 +40,10 @@ namespace PuppeteerSharp.Helpers.Json; [JsonSerializable(typeof(BasicFrameResponse))] [JsonSerializable(typeof(BindingCalledResponse))] [JsonSerializable(typeof(BrowserGetVersionResponse))] +[JsonSerializable(typeof(BrowserGetWindowForTargetResponse))] [JsonSerializable(typeof(BrowserGrantPermissionsRequest))] [JsonSerializable(typeof(BrowserResetPermissionsRequest))] +[JsonSerializable(typeof(BrowserSetContentsSizeRequest))] [JsonSerializable(typeof(BoundingBox[]))] [JsonSerializable(typeof(BoundingBox))] [JsonSerializable(typeof(CertificateErrorResponse))] diff --git a/lib/PuppeteerSharp/IPage.cs b/lib/PuppeteerSharp/IPage.cs index 4ea9de04d..ff36dfade 100644 --- a/lib/PuppeteerSharp/IPage.cs +++ b/lib/PuppeteerSharp/IPage.cs @@ -1449,6 +1449,17 @@ public interface IPage : IDisposable, IAsyncDisposable /// A task that resolves after the page gets the prompt. Task WaitForDevicePromptAsync(WaitForOptions options = null); + /// + /// Resizes the browser window of this page so that the content area (excluding browser UI) has the specified width and height. + /// + /// The desired width of the content area in pixels. + /// The desired height of the content area in pixels. + /// A task that resolves when the resize operation is complete. + /// + /// This is an experimental API. + /// + Task ResizeAsync(int contentWidth, int contentHeight); + /// /// , , and can accept an optional `priority` to activate Cooperative Intercept Mode. /// In Cooperative Mode, all interception tasks are guaranteed to run and all async handlers are awaited. diff --git a/lib/PuppeteerSharp/Page.cs b/lib/PuppeteerSharp/Page.cs index f5ae50e61..6a247f379 100644 --- a/lib/PuppeteerSharp/Page.cs +++ b/lib/PuppeteerSharp/Page.cs @@ -263,6 +263,9 @@ public Task WaitForDevicePromptAsync( WaitForOptions options = default(WaitForOptions)) => MainFrame.WaitForDevicePromptAsync(options); + /// + public abstract Task ResizeAsync(int contentWidth, int contentHeight); + /// public Task EvaluateExpressionHandleAsync(string script) => MainFrame.EvaluateExpressionHandleAsync(script); diff --git a/lib/PuppeteerSharp/PageAccessibility/Accessibility.cs b/lib/PuppeteerSharp/PageAccessibility/Accessibility.cs index 3568f7e30..703857b9f 100644 --- a/lib/PuppeteerSharp/PageAccessibility/Accessibility.cs +++ b/lib/PuppeteerSharp/PageAccessibility/Accessibility.cs @@ -33,7 +33,9 @@ public async Task SnapshotAsync(AccessibilitySnapshotOptions o var needle = defaultRoot; if (backendNodeId != null) { - needle = defaultRoot.Find(node => node.Payload.BackendDOMNodeId.GetInt32().Equals(backendNodeId.Value.GetInt32())); + needle = defaultRoot.Find(node => + node.Payload.BackendDOMNodeId.ValueKind == JsonValueKind.Number && + node.Payload.BackendDOMNodeId.GetInt32() == backendNodeId.Value.GetInt32()); if (needle == null) { return null; @@ -47,12 +49,9 @@ public async Task SnapshotAsync(AccessibilitySnapshotOptions o var interestingNodes = new List(); CollectInterestingNodes(interestingNodes, defaultRoot, false); - if (!interestingNodes.Contains(needle)) - { - return null; - } - return SerializeTree(needle, interestingNodes)[0]; + var result = SerializeTree(needle, interestingNodes); + return result.Length > 0 ? result[0] : null; } internal void UpdateClient(CDPSession client) => _client = client;