From 739d177dae763eecf906e230819e1116fafd37f7 Mon Sep 17 00:00:00 2001 From: jimevans Date: Tue, 29 Aug 2023 11:40:19 -0400 Subject: [PATCH] [dotnet] Refactor WebSocket communication for BiDi (#12614) * [dotnet] Refactor WebSocket communication for BiDi Replaces the existing WebSocket communication mechanism with one more robust. Rather than immediately relying on event handlers to react to events and command reponses, it writes the incoming data to a queue which is read from a different thread. This eliminates the issue where the user might have multiple simultaneous sends or receives to the WebSocket while their event handler is running. It also dispatches incoming events on different threads for the same reason. This should eliminate at least some of the issues surrounding socket communication with bidirectional features, whether implemented using CDP or the WebDriver BiDi protocol. * Address review comments * Removing use of System.Threading.Channels * nitpick: fix XML doc comment * Simplify WebSocket message queue processing code * Omit added test from Firefox * revert add of now unused nuget packages --- .../src/webdriver/DevTools/DevToolsSession.cs | 198 ++++-------- .../webdriver/DevTools/WebSocketConnection.cs | 282 ++++++++++++++++++ ...ebSocketConnectionDataReceivedEventArgs.cs | 44 +++ dotnet/src/webdriver/WebDriver.csproj | 9 - .../NightlyChannelFirefoxDriver.cs | 2 +- .../StableChannelFirefoxDriver.cs | 2 +- .../test/common/NetworkInterceptionTests.cs | 37 +++ 7 files changed, 422 insertions(+), 152 deletions(-) create mode 100644 dotnet/src/webdriver/DevTools/WebSocketConnection.cs create mode 100644 dotnet/src/webdriver/DevTools/WebSocketConnectionDataReceivedEventArgs.cs create mode 100644 dotnet/test/common/NetworkInterceptionTests.cs diff --git a/dotnet/src/webdriver/DevTools/DevToolsSession.cs b/dotnet/src/webdriver/DevTools/DevToolsSession.cs index aad579a7b1ed7..1d6466c2436fa 100644 --- a/dotnet/src/webdriver/DevTools/DevToolsSession.cs +++ b/dotnet/src/webdriver/DevTools/DevToolsSession.cs @@ -19,10 +19,7 @@ using System; using System.Collections.Concurrent; using System.Globalization; -using System.IO; using System.Net.Http; -using System.Net.WebSockets; -using System.Text; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; @@ -50,15 +47,14 @@ public class DevToolsSession : IDevToolsSession private bool isDisposed = false; private string attachedTargetId; - private ClientWebSocket sessionSocket; + private WebSocketConnection connection; private ConcurrentDictionary pendingCommands = new ConcurrentDictionary(); + private readonly BlockingCollection messageQueue = new BlockingCollection(); + private readonly Task messageQueueMonitorTask; private long currentCommandId = 0; private DevToolsDomains domains; - private CancellationTokenSource receiveCancellationToken; - private Task receiveTask; - /// /// Initializes a new instance of the DevToolsSession class, using the specified WebSocket endpoint. /// @@ -76,6 +72,8 @@ public DevToolsSession(string endpointAddress) { this.websocketAddress = endpointAddress; } + this.messageQueueMonitorTask = Task.Run(() => this.MonitorMessageQueue()); + this.messageQueueMonitorTask.ConfigureAwait(false); } /// @@ -213,15 +211,13 @@ public T GetVersionSpecificDomains() where T : DevToolsSessionDomains var message = new DevToolsCommandData(Interlocked.Increment(ref this.currentCommandId), this.ActiveSessionId, commandName, commandParameters); - if (this.sessionSocket != null && this.sessionSocket.State == WebSocketState.Open) + if (this.connection != null && this.connection.IsActive) { LogTrace("Sending {0} {1}: {2}", message.CommandId, message.CommandName, commandParameters.ToString()); - var contents = JsonConvert.SerializeObject(message); - var contentBuffer = Encoding.UTF8.GetBytes(contents); - + string contents = JsonConvert.SerializeObject(message); this.pendingCommands.TryAdd(message.CommandId, message); - await this.sessionSocket.SendAsync(new ArraySegment(contentBuffer), WebSocketMessageType.Text, true, cancellationToken); + await this.connection.SendData(contents); var responseWasReceived = await Task.Run(() => message.SyncEvent.Wait(millisecondsTimeout.Value, cancellationToken)); @@ -230,8 +226,7 @@ public T GetVersionSpecificDomains() where T : DevToolsSessionDomains throw new InvalidOperationException($"A command response was not received: {commandName}"); } - DevToolsCommandData modified; - if (this.pendingCommands.TryRemove(message.CommandId, out modified)) + if (this.pendingCommands.TryRemove(message.CommandId, out DevToolsCommandData modified)) { if (modified.IsError) { @@ -256,10 +251,7 @@ public T GetVersionSpecificDomains() where T : DevToolsSessionDomains } else { - if (this.sessionSocket != null) - { - LogTrace("WebSocket is not connected (current state is {0}); not sending {1}", this.sessionSocket.State, message.CommandName); - } + LogTrace("WebSocket is not connected; not sending {0}", message.CommandName); } return null; @@ -330,11 +322,7 @@ protected void Dispose(bool disposing) { this.Domains.Target.TargetDetached -= this.OnTargetDetached; this.pendingCommands.Clear(); - this.TerminateSocketConnection(); - - // Note: Canceling the receive task will dispose of - // the underlying ClientWebSocket instance. - this.CancelReceiveTask(); + this.TerminateSocketConnection().GetAwaiter().GetResult(); } this.isDisposed = true; @@ -377,28 +365,6 @@ private async Task InitializeProtocol(int requestedProtocolVersion) return protocolVersion; } - private async Task InitializeSocketConnection() - { - LogTrace("Creating WebSocket"); - this.sessionSocket = new ClientWebSocket(); - this.sessionSocket.Options.KeepAliveInterval = TimeSpan.Zero; - - try - { - var timeoutTokenSource = new CancellationTokenSource(this.openConnectionWaitTimeSpan); - await this.sessionSocket.ConnectAsync(new Uri(this.websocketAddress), timeoutTokenSource.Token); - while (this.sessionSocket.State != WebSocketState.Open && !timeoutTokenSource.Token.IsCancellationRequested) ; - } - catch (OperationCanceledException e) - { - throw new WebDriverException(string.Format(CultureInfo.InvariantCulture, "Could not establish WebSocket connection within {0} seconds.", this.openConnectionWaitTimeSpan.TotalSeconds), e); - } - - LogTrace("WebSocket created; starting message listener"); - this.receiveCancellationToken = new CancellationTokenSource(); - this.receiveTask = Task.Run(() => ReceiveMessage().ConfigureAwait(false)); - } - private async Task InitializeSession() { LogTrace("Creating session"); @@ -445,116 +411,56 @@ private void OnTargetDetached(object sender, TargetDetachedEventArgs e) } } - private void TerminateSocketConnection() + private async Task InitializeSocketConnection() { - if (this.sessionSocket != null && this.sessionSocket.State == WebSocketState.Open) - { - var closeConnectionTokenSource = new CancellationTokenSource(this.closeConnectionWaitTimeSpan); - try - { - // Since Chromium-based DevTools does not respond to the close - // request with a correctly echoed WebSocket close packet, but - // rather just terminates the socket connection, so we have to - // catch the exception thrown when the socket is terminated - // unexpectedly. Also, because we are using async, waiting for - // the task to complete might throw a TaskCanceledException, - // which we should also catch. Additiionally, there are times - // when mulitple failure modes can be seen, which will throw an - // AggregateException, consolidating several exceptions into one, - // and this too must be caught. Finally, the call to CloseAsync - // will hang even though the connection is already severed. - // Wait for the task to complete for a short time (since we're - // restricted to localhost, the default of 2 seconds should be - // plenty; if not, change the initialization of the timout), - // and if the task is still running, then we assume the connection - // is properly closed. - LogTrace("Sending socket close request"); - Task closeTask = Task.Run(async () => await this.sessionSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, closeConnectionTokenSource.Token)); - closeTask.Wait(); - } - catch (WebSocketException) - { - } - catch (TaskCanceledException) - { - } - catch (AggregateException) - { - } - } + LogTrace("Creating WebSocket"); + this.connection = new WebSocketConnection(this.openConnectionWaitTimeSpan, this.closeConnectionWaitTimeSpan); + connection.DataReceived += OnConnectionDataReceived; + await connection.Start(this.websocketAddress); + LogTrace("WebSocket created"); } - private void CancelReceiveTask() + private async Task TerminateSocketConnection() { - if (this.receiveTask != null) + LogTrace("Closing WebSocket"); + if (this.connection != null && this.connection.IsActive) { - // Wait for the recieve task to be completely exited (for - // whatever reason) before attempting to dispose it. Also - // note that canceling the receive task will dispose of the - // underlying WebSocket. - this.receiveCancellationToken.Cancel(); - this.receiveTask.Wait(); - this.receiveTask.Dispose(); - this.receiveTask = null; + await this.connection.Stop(); + await this.ShutdownMessageQueue(); } + LogTrace("WebSocket closed"); } - private async Task ReceiveMessage() + private async Task ShutdownMessageQueue() { - var cancellationToken = this.receiveCancellationToken.Token; - try - { - var buffer = WebSocket.CreateClientBuffer(1024, 1024); - while (this.sessionSocket.State != WebSocketState.Closed && !cancellationToken.IsCancellationRequested) - { - WebSocketReceiveResult result = await this.sessionSocket.ReceiveAsync(buffer, cancellationToken); - if (!cancellationToken.IsCancellationRequested) - { - if (result.MessageType == WebSocketMessageType.Close && this.sessionSocket.State == WebSocketState.CloseReceived) - { - LogTrace("Got WebSocket close message from browser"); - await this.sessionSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, cancellationToken); - } - } - - if (this.sessionSocket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close) - { - using (var stream = new MemoryStream()) - { - stream.Write(buffer.Array, 0, result.Count); - while (!result.EndOfMessage) - { - result = await this.sessionSocket.ReceiveAsync(buffer, cancellationToken); - stream.Write(buffer.Array, 0, result.Count); - } - - stream.Seek(0, SeekOrigin.Begin); - using (var reader = new StreamReader(stream, Encoding.UTF8)) - { - string message = reader.ReadToEnd(); - - // fire and forget - // TODO: we need implement some kind of queue - Task.Run(() => ProcessIncomingMessage(message)); - } - } - } - } - } - catch (OperationCanceledException) + // THe WebSockect connection is always closed before this method + // is called, so there will eventually be no more data written + // into the message queue, meaning this loop should be guaranteed + // to complete. + while (this.connection.IsActive) { + await Task.Delay(TimeSpan.FromMilliseconds(10)); } - catch (WebSocketException) - { - } - finally + + this.messageQueue.CompleteAdding(); + await this.messageQueueMonitorTask; + } + + private void MonitorMessageQueue() + { + // GetConsumingEnumerable blocks until if BlockingCollection.IsCompleted + // is false (i.e., is still able to be written to), and there are no items + // in the collection. Once any items are added to the collection, the method + // unblocks and we can process any items in the collection at that moment. + // Once IsCompleted is true, the method unblocks with no items in returned + // in the IEnumerable, meaning the foreach loop will terminate gracefully. + foreach (string message in this.messageQueue.GetConsumingEnumerable()) { - this.sessionSocket.Dispose(); - this.sessionSocket = null; + this.ProcessMessage(message); } } - private void ProcessIncomingMessage(string message) + private void ProcessMessage(string message) { var messageObject = JObject.Parse(message); @@ -594,7 +500,12 @@ private void ProcessIncomingMessage(string message) LogTrace("Recieved Event {0}: {1}", method, eventData.ToString()); - OnDevToolsEventReceived(new DevToolsEventReceivedEventArgs(methodParts[0], methodParts[1], eventData)); + // Dispatch the event on a new thread so that any event handlers + // responding to the event will not block this thread from processing + // DevTools commands that may be sent in the body of the attached + // event handler. If thread pool starvation seems to become a problem, + // we can switch to a channel-based queue. + Task.Run(() => OnDevToolsEventReceived(new DevToolsEventReceivedEventArgs(methodParts[0], methodParts[1], eventData))); return; } @@ -610,6 +521,11 @@ private void OnDevToolsEventReceived(DevToolsEventReceivedEventArgs e) } } + private void OnConnectionDataReceived(object sender, WebSocketConnectionDataReceivedEventArgs e) + { + this.messageQueue.Add(e.Data); + } + private void LogTrace(string message, params object[] args) { if (LogMessage != null) diff --git a/dotnet/src/webdriver/DevTools/WebSocketConnection.cs b/dotnet/src/webdriver/DevTools/WebSocketConnection.cs new file mode 100644 index 0000000000000..939a334f931ee --- /dev/null +++ b/dotnet/src/webdriver/DevTools/WebSocketConnection.cs @@ -0,0 +1,282 @@ +// +// 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.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace OpenQA.Selenium.DevTools +{ + /// + /// Represents a connection to a WebDriver Bidi remote end. + /// + public class WebSocketConnection + { + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10); + private readonly CancellationTokenSource clientTokenSource = new CancellationTokenSource(); + private readonly TimeSpan startupTimeout; + private readonly TimeSpan shutdownTimeout; + private readonly int bufferSize = 4096; + private Task dataReceiveTask; + private bool isActive = false; + private ClientWebSocket client = new ClientWebSocket(); + + /// + /// Initializes a new instance of the class. + /// + public WebSocketConnection() + : this(DefaultTimeout) + { + } + + /// + /// Initializes a new instance of the class with a given startup timeout. + /// + /// The timeout before throwing an error when starting up the connection. + public WebSocketConnection(TimeSpan startupTimeout) + : this(startupTimeout, DefaultTimeout) + { + } + + /// + /// Initializes a new instance of the class with a given startup and shutdown timeout. + /// + /// The timeout before throwing an error when starting up the connection. + /// The timeout before throwing an error when shutting down the connection. + public WebSocketConnection(TimeSpan startupTimeout, TimeSpan shutdownTimeout) + { + this.startupTimeout = startupTimeout; + this.shutdownTimeout = shutdownTimeout; + } + + /// + /// Occurs when data is received from this connection. + /// + public event EventHandler DataReceived; + + /// + /// Occurs when a log message is emitted from this connection. + /// + public event EventHandler LogMessage; + + /// + /// Gets a value indicating whether this connection is active. + /// + public bool IsActive => this.isActive; + + /// + /// Gets the buffer size for communication used by this connection. + /// + public int BufferSize => this.bufferSize; + + /// + /// Asynchronously starts communication with the remote end of this connection. + /// + /// The URL used to connect to the remote end. + /// The task object representing the asynchronous operation. + /// Thrown when the connection is not established within the startup timeout. + public virtual async Task Start(string url) + { + this.Log($"Opening connection to URL {url}", DevToolsSessionLogLevel.Trace); + bool connected = false; + DateTime timeout = DateTime.Now.Add(this.startupTimeout); + while (!connected && DateTime.Now <= timeout) + { + try + { + await this.client.ConnectAsync(new Uri(url), this.clientTokenSource.Token); + connected = true; + } + catch (WebSocketException) + { + // If the server-side socket is not yet ready, it leaves the client socket in a closed state, + // which sees the object as disposed, so we must create a new one to try again + await Task.Delay(TimeSpan.FromMilliseconds(500)); + this.client = new ClientWebSocket(); + } + } + + if (!connected) + { + throw new TimeoutException($"Could not connect to browser within {this.startupTimeout.TotalSeconds} seconds"); + } + + this.dataReceiveTask = Task.Run(async () => await this.ReceiveData()); + this.isActive = true; + this.Log($"Connection opened", DevToolsSessionLogLevel.Trace); + } + + /// + /// Asynchronously stops communication with the remote end of this connection. + /// + /// The task object representing the asynchronous operation. + public virtual async Task Stop() + { + this.Log($"Closing connection", DevToolsSessionLogLevel.Trace); + if (this.client.State != WebSocketState.Open) + { + this.Log($"Socket already closed (Socket state: {this.client.State})"); + } + else + { + await this.CloseClientWebSocket(); + } + + // Whether we closed the socket or timed out, we cancel the token causing ReceiveAsync to abort the socket. + // The finally block at the end of the processing loop will dispose of the ClientWebSocket object. + this.clientTokenSource.Cancel(); + if (this.dataReceiveTask != null) + { + await this.dataReceiveTask; + } + } + + /// + /// Asynchronously sends data to the remote end of this connection. + /// + /// The data to be sent to the remote end of this connection. + /// The task object representing the asynchronous operation. + public virtual async Task SendData(string data) + { + ArraySegment messageBuffer = new ArraySegment(Encoding.UTF8.GetBytes(data)); + this.Log($"SEND >>> {data}"); + await this.client.SendAsync(messageBuffer, WebSocketMessageType.Text, endOfMessage: true, CancellationToken.None); + } + + /// + /// Asynchronously closes the client WebSocket. + /// + /// The task object representing the asynchronous operation. + protected virtual async Task CloseClientWebSocket() + { + // Close the socket first, because ReceiveAsync leaves an invalid socket (state = aborted) when the token is cancelled + CancellationTokenSource timeout = new CancellationTokenSource(this.shutdownTimeout); + try + { + // After this, the socket state which change to CloseSent + await this.client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", timeout.Token); + + // Now we wait for the server response, which will close the socket + while (this.client.State != WebSocketState.Closed && this.client.State != WebSocketState.Aborted && !timeout.Token.IsCancellationRequested) + { + // The loop may be too tight for the cancellation token to get triggered, so add a small delay + await Task.Delay(TimeSpan.FromMilliseconds(10)); + } + + this.Log($"Client state is {this.client.State}", DevToolsSessionLogLevel.Trace); + } + catch (OperationCanceledException) + { + // An OperationCanceledException is normal upon task/token cancellation, so disregard it + } + } + + /// + /// Raises the DataReceived event. + /// + /// The event args used when raising the event. + protected virtual void OnDataReceived(WebSocketConnectionDataReceivedEventArgs e) + { + if (this.DataReceived != null) + { + this.DataReceived(this, e); + } + } + + /// + /// Raises the LogMessage event. + /// + /// The event args used when raising the event. + protected virtual void OnLogMessage(DevToolsSessionLogMessageEventArgs e) + { + if (this.LogMessage != null) + { + this.LogMessage(this, e); + } + } + + private async Task ReceiveData() + { + CancellationToken cancellationToken = this.clientTokenSource.Token; + try + { + StringBuilder messageBuilder = new StringBuilder(); + ArraySegment buffer = WebSocket.CreateClientBuffer(this.bufferSize, this.bufferSize); + while (this.client.State != WebSocketState.Closed && !cancellationToken.IsCancellationRequested) + { + WebSocketReceiveResult receiveResult = await this.client.ReceiveAsync(buffer, cancellationToken); + + // If the token is cancelled while ReceiveAsync is blocking, the socket state changes to aborted and it can't be used + if (!cancellationToken.IsCancellationRequested) + { + // The server is notifying us that the connection will close, and we did + // not initiate the close; send acknowledgement + if (receiveResult.MessageType == WebSocketMessageType.Close && this.client.State != WebSocketState.Closed && this.client.State != WebSocketState.CloseSent) + { + this.Log($"Acknowledging Close frame received from server (client state: {this.client.State})", DevToolsSessionLogLevel.Trace); + await this.client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Acknowledge Close frame", CancellationToken.None); + } + + // Display text or binary data + if (this.client.State == WebSocketState.Open && receiveResult.MessageType != WebSocketMessageType.Close) + { + messageBuilder.Append(Encoding.UTF8.GetString(buffer.Array, 0, receiveResult.Count)); + if (receiveResult.EndOfMessage) + { + string message = messageBuilder.ToString(); + messageBuilder = new StringBuilder(); + if (message.Length > 0) + { + this.Log($"RECV <<< {message}"); + this.OnDataReceived(new WebSocketConnectionDataReceivedEventArgs(message)); + } + } + } + } + } + + this.Log($"Ending processing loop in state {this.client.State}", DevToolsSessionLogLevel.Trace); + } + catch (OperationCanceledException) + { + // An OperationCanceledException is normal upon task/token cancellation, so disregard it + } + catch (WebSocketException e) + { + this.Log($"Unexpected error during receive of data: {e.Message}", DevToolsSessionLogLevel.Error); + } + finally + { + this.client.Dispose(); + this.isActive = false; + } + } + + private void Log(string message) + { + this.Log(message, DevToolsSessionLogLevel.Trace); + } + + private void Log(string message, DevToolsSessionLogLevel level) + { + this.OnLogMessage(new DevToolsSessionLogMessageEventArgs(level, "[{0}] {1}", "Connection", message)); + } + } +} diff --git a/dotnet/src/webdriver/DevTools/WebSocketConnectionDataReceivedEventArgs.cs b/dotnet/src/webdriver/DevTools/WebSocketConnectionDataReceivedEventArgs.cs new file mode 100644 index 0000000000000..4cbe597587291 --- /dev/null +++ b/dotnet/src/webdriver/DevTools/WebSocketConnectionDataReceivedEventArgs.cs @@ -0,0 +1,44 @@ +// +// 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.DevTools +{ + /// + /// Object containing event data for events raised when data is received from a WebDriver Bidi connection. + /// + public class WebSocketConnectionDataReceivedEventArgs : EventArgs + { + private readonly string data; + + /// + /// Initializes a new instance of the class. + /// + /// The data received from the connection. + public WebSocketConnectionDataReceivedEventArgs(string data) + { + this.data = data; + } + + /// + /// Gets the data received from the connection. + /// + public string Data => this.data; + } +} diff --git a/dotnet/src/webdriver/WebDriver.csproj b/dotnet/src/webdriver/WebDriver.csproj index 4aa2050966f90..f174af0af868a 100644 --- a/dotnet/src/webdriver/WebDriver.csproj +++ b/dotnet/src/webdriver/WebDriver.csproj @@ -48,15 +48,6 @@ - - - - - - - - - diff --git a/dotnet/test/common/CustomDriverConfigs/NightlyChannelFirefoxDriver.cs b/dotnet/test/common/CustomDriverConfigs/NightlyChannelFirefoxDriver.cs index c61fb91e861c2..6543ad38dbb82 100644 --- a/dotnet/test/common/CustomDriverConfigs/NightlyChannelFirefoxDriver.cs +++ b/dotnet/test/common/CustomDriverConfigs/NightlyChannelFirefoxDriver.cs @@ -23,7 +23,7 @@ public NightlyChannelFirefoxDriver(FirefoxDriverService service, FirefoxOptions public static FirefoxOptions DefaultOptions { - get { return new FirefoxOptions() { BrowserVersion = "nightly" }; } + get { return new FirefoxOptions() { BrowserVersion = "nightly", AcceptInsecureCertificates = true, EnableDevToolsProtocol = true }; } } } } diff --git a/dotnet/test/common/CustomDriverConfigs/StableChannelFirefoxDriver.cs b/dotnet/test/common/CustomDriverConfigs/StableChannelFirefoxDriver.cs index 5577d4a8963f9..49bae45c931ff 100644 --- a/dotnet/test/common/CustomDriverConfigs/StableChannelFirefoxDriver.cs +++ b/dotnet/test/common/CustomDriverConfigs/StableChannelFirefoxDriver.cs @@ -23,7 +23,7 @@ public StableChannelFirefoxDriver(FirefoxDriverService service, FirefoxOptions o public static FirefoxOptions DefaultOptions { - get { return new FirefoxOptions() { AcceptInsecureCertificates = true }; } + get { return new FirefoxOptions() { AcceptInsecureCertificates = true, EnableDevToolsProtocol = true }; } } } } diff --git a/dotnet/test/common/NetworkInterceptionTests.cs b/dotnet/test/common/NetworkInterceptionTests.cs new file mode 100644 index 0000000000000..6d29a8d41eaeb --- /dev/null +++ b/dotnet/test/common/NetworkInterceptionTests.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using OpenQA.Selenium.DevTools; + +namespace OpenQA.Selenium +{ + [TestFixture] + public class NetworkInterceptionTests : DriverTestFixture + { + [Test] + [IgnoreBrowser(Browser.Firefox, "Firefox does not implement the CDP Fetch domain required for network interception")] + public async Task TestCanInterceptNetworkCalls() + { + if (driver is IDevTools) + { + INetwork network = driver.Manage().Network; + NetworkResponseHandler handler = new NetworkResponseHandler(); + handler.ResponseMatcher = (responseData) => responseData.Url.Contains("simpleTest.html"); + handler.ResponseTransformer = (responseData) => + { + responseData.Body = "

I intercepted you

"; + return responseData; + }; + network.AddResponseHandler(handler); + await network.StartMonitoring(); + driver.Url = simpleTestPage; + string text = driver.FindElement(By.CssSelector("p")).Text; + await network.StopMonitoring(); + Assert.AreEqual("I intercepted you", text); + } + } + } +}