diff --git a/MODULE.bazel b/MODULE.bazel index 348c66a2bf10d..bd8225d29fc95 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -17,7 +17,7 @@ bazel_dep(name = "protobuf", version = "21.7", dev_dependency = True, repo_name # Required for rules_rust to import the crates properly bazel_dep(name = "rules_cc", version = "0.0.9", dev_dependency = True) -bazel_dep(name = "rules_dotnet", version = "0.15.1") +bazel_dep(name = "rules_dotnet", version = "0.16.0") bazel_dep(name = "rules_java", version = "7.11.1") bazel_dep(name = "rules_jvm_external", version = "6.3") bazel_dep(name = "rules_nodejs", version = "6.2.0") @@ -178,8 +178,7 @@ maven.install( "com.google.auto.service:auto-service:1.1.1", "com.google.auto.service:auto-service-annotations:1.1.1", "com.google.googlejavaformat:google-java-format:jar:1.23.0", - "com.graphql-java:graphql-java:20.2", - "com.graphql-java:java-dataloader:3.2.0", + "com.graphql-java:graphql-java:22.3", "dev.failsafe:failsafe:3.3.2", "io.grpc:grpc-context:1.66.0", "io.lettuce:lettuce-core:6.4.0.RELEASE", diff --git a/README.md b/README.md index e5ef14de8c4f4..637c6428c3198 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Selenium [![CI](https://github.com/SeleniumHQ/selenium/actions/workflows/ci.yml/badge.svg?branch=trunk&event=schedule)](https://github.com/SeleniumHQ/selenium/actions/workflows/ci.yml) +[![Releases downloads](https://img.shields.io/github/downloads/SeleniumHQ/selenium/total.svg)](https://github.com/SeleniumHQ/selenium/releases) Selenium Logo diff --git a/dotnet/src/support/UI/LoadableComponentException.cs b/dotnet/src/support/UI/LoadableComponentException.cs index 64354a240ac12..305543f50a3cd 100644 --- a/dotnet/src/support/UI/LoadableComponentException.cs +++ b/dotnet/src/support/UI/LoadableComponentException.cs @@ -57,17 +57,5 @@ public LoadableComponentException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected LoadableComponentException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/support/UI/UnexpectedTagNameException.cs b/dotnet/src/support/UI/UnexpectedTagNameException.cs index 9cea8a3781e2b..9fc85a8f39bb7 100644 --- a/dotnet/src/support/UI/UnexpectedTagNameException.cs +++ b/dotnet/src/support/UI/UnexpectedTagNameException.cs @@ -69,17 +69,5 @@ public UnexpectedTagNameException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected UnexpectedTagNameException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/BUILD.bazel b/dotnet/src/webdriver/BUILD.bazel index 1f23755fa1025..afcdd0785bae4 100644 --- a/dotnet/src/webdriver/BUILD.bazel +++ b/dotnet/src/webdriver/BUILD.bazel @@ -55,6 +55,7 @@ csharp_library( framework("nuget", "Microsoft.Bcl.AsyncInterfaces"), framework("nuget", "System.Threading.Tasks.Extensions"), framework("nuget", "System.Memory"), + framework("nuget", "System.Text.Encodings.Web"), framework("nuget", "System.Text.Json"), ], ) @@ -119,6 +120,7 @@ csharp_library( framework("nuget", "Microsoft.Bcl.AsyncInterfaces"), framework("nuget", "System.Threading.Tasks.Extensions"), framework("nuget", "System.Memory"), + framework("nuget", "System.Text.Encodings.Web"), framework("nuget", "System.Text.Json"), ], ) diff --git a/dotnet/src/webdriver/DetachedShadowRootException.cs b/dotnet/src/webdriver/DetachedShadowRootException.cs index 43608ca8358d7..d8af8c148db6f 100644 --- a/dotnet/src/webdriver/DetachedShadowRootException.cs +++ b/dotnet/src/webdriver/DetachedShadowRootException.cs @@ -57,17 +57,5 @@ public DetachedShadowRootException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected DetachedShadowRootException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/DevTools/DevToolsSession.cs b/dotnet/src/webdriver/DevTools/DevToolsSession.cs index 860074ecd2848..931fb22dce019 100644 --- a/dotnet/src/webdriver/DevTools/DevToolsSession.cs +++ b/dotnet/src/webdriver/DevTools/DevToolsSession.cs @@ -16,6 +16,7 @@ // limitations under the License. // +using OpenQA.Selenium.Internal.Logging; using System; using System.Collections.Concurrent; using System.Globalization; @@ -56,6 +57,8 @@ public class DevToolsSession : IDevToolsSession private DevToolsDomains domains; private readonly DevToolsOptions options; + private readonly static ILogger logger = Internal.Logging.Log.GetLogger(); + /// /// Initializes a new instance of the DevToolsSession class, using the specified WebSocket endpoint. /// @@ -272,6 +275,11 @@ public T GetVersionSpecificDomains() where T : DevToolsSessionDomains if (this.connection != null && this.connection.IsActive) { + if (logger.IsEnabled(LogEventLevel.Trace)) + { + logger.Trace($"CDP SND >> {message.CommandId} {message.CommandName}: {commandParameters.ToJsonString()}"); + } + LogTrace("Sending {0} {1}: {2}", message.CommandId, message.CommandName, commandParameters.ToString()); string contents = JsonSerializer.Serialize(message); @@ -540,6 +548,11 @@ private void MonitorMessageQueue() private void ProcessMessage(string message) { + if (logger.IsEnabled(LogEventLevel.Trace)) + { + logger.Trace($"CDP RCV << {message}"); + } + var messageObject = JsonObject.Parse(message).AsObject(); if (messageObject.TryGetPropertyValue("id", out var idProperty)) @@ -583,7 +596,22 @@ private void ProcessMessage(string message) // 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))); + Task.Run(() => + { + try + { + OnDevToolsEventReceived(new DevToolsEventReceivedEventArgs(methodParts[0], methodParts[1], eventData)); + } + catch (Exception ex) + { + if (logger.IsEnabled(LogEventLevel.Warn)) + { + logger.Warn($"CDP VNT ^^ Unhandled error occured in event handler of '{method}' method. {ex}"); + } + + throw; + } + }); return; } diff --git a/dotnet/src/webdriver/DriverServiceNotFoundException.cs b/dotnet/src/webdriver/DriverServiceNotFoundException.cs index 25ad25cb6d9bc..2094d61cc2761 100644 --- a/dotnet/src/webdriver/DriverServiceNotFoundException.cs +++ b/dotnet/src/webdriver/DriverServiceNotFoundException.cs @@ -57,17 +57,5 @@ public DriverServiceNotFoundException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected DriverServiceNotFoundException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/ElementClickInterceptedException.cs b/dotnet/src/webdriver/ElementClickInterceptedException.cs index 12d073ed66c0f..f937dc5e58af9 100644 --- a/dotnet/src/webdriver/ElementClickInterceptedException.cs +++ b/dotnet/src/webdriver/ElementClickInterceptedException.cs @@ -57,17 +57,5 @@ public ElementClickInterceptedException(string message, Exception innerException : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected ElementClickInterceptedException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/ElementNotInteractableException.cs b/dotnet/src/webdriver/ElementNotInteractableException.cs index f7ccf80ca5e5d..65ae5395e2207 100644 --- a/dotnet/src/webdriver/ElementNotInteractableException.cs +++ b/dotnet/src/webdriver/ElementNotInteractableException.cs @@ -57,17 +57,5 @@ public ElementNotInteractableException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected ElementNotInteractableException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/ElementNotSelectableException.cs b/dotnet/src/webdriver/ElementNotSelectableException.cs index 3574fc6671a77..68fa2b7f4ad6e 100644 --- a/dotnet/src/webdriver/ElementNotSelectableException.cs +++ b/dotnet/src/webdriver/ElementNotSelectableException.cs @@ -57,17 +57,5 @@ public ElementNotSelectableException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected ElementNotSelectableException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/ElementNotVisibleException.cs b/dotnet/src/webdriver/ElementNotVisibleException.cs index 8f27ec01cfa07..43f815ae105ad 100644 --- a/dotnet/src/webdriver/ElementNotVisibleException.cs +++ b/dotnet/src/webdriver/ElementNotVisibleException.cs @@ -57,17 +57,5 @@ public ElementNotVisibleException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected ElementNotVisibleException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/InsecureCertificateException.cs b/dotnet/src/webdriver/InsecureCertificateException.cs index d8cfd9bd71f16..37da31170bf21 100644 --- a/dotnet/src/webdriver/InsecureCertificateException.cs +++ b/dotnet/src/webdriver/InsecureCertificateException.cs @@ -57,17 +57,5 @@ public InsecureCertificateException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected InsecureCertificateException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/InvalidCookieDomainException.cs b/dotnet/src/webdriver/InvalidCookieDomainException.cs index 3e6debb2d72a7..0b9294bb6d5bd 100644 --- a/dotnet/src/webdriver/InvalidCookieDomainException.cs +++ b/dotnet/src/webdriver/InvalidCookieDomainException.cs @@ -57,17 +57,5 @@ public InvalidCookieDomainException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected InvalidCookieDomainException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/InvalidElementStateException.cs b/dotnet/src/webdriver/InvalidElementStateException.cs index f0f2fed91c3a3..1010a9f084d8f 100644 --- a/dotnet/src/webdriver/InvalidElementStateException.cs +++ b/dotnet/src/webdriver/InvalidElementStateException.cs @@ -57,17 +57,5 @@ public InvalidElementStateException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected InvalidElementStateException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/InvalidSelectorException.cs b/dotnet/src/webdriver/InvalidSelectorException.cs index 9cdf2c3e66f17..509dc2d74a785 100644 --- a/dotnet/src/webdriver/InvalidSelectorException.cs +++ b/dotnet/src/webdriver/InvalidSelectorException.cs @@ -63,18 +63,6 @@ public InvalidSelectorException(string message, Exception innerException) { } - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected InvalidSelectorException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - /// /// Add information about obtaining additional support from documentation to this exception. /// diff --git a/dotnet/src/webdriver/JavaScriptException.cs b/dotnet/src/webdriver/JavaScriptException.cs index b852e904bfc5e..79045e4093c08 100644 --- a/dotnet/src/webdriver/JavaScriptException.cs +++ b/dotnet/src/webdriver/JavaScriptException.cs @@ -57,17 +57,5 @@ public JavaScriptException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected JavaScriptException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/MoveTargetOutOfBoundsException.cs b/dotnet/src/webdriver/MoveTargetOutOfBoundsException.cs index 7394ce444715d..d2cbd4ea1f1c0 100644 --- a/dotnet/src/webdriver/MoveTargetOutOfBoundsException.cs +++ b/dotnet/src/webdriver/MoveTargetOutOfBoundsException.cs @@ -58,17 +58,5 @@ public MoveTargetOutOfBoundsException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected MoveTargetOutOfBoundsException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/NoAlertPresentException.cs b/dotnet/src/webdriver/NoAlertPresentException.cs index 5573c60308fdb..72c9600280e55 100644 --- a/dotnet/src/webdriver/NoAlertPresentException.cs +++ b/dotnet/src/webdriver/NoAlertPresentException.cs @@ -57,17 +57,5 @@ public NoAlertPresentException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected NoAlertPresentException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/NoSuchDriverException.cs b/dotnet/src/webdriver/NoSuchDriverException.cs index c665131741982..9bd0bf09be271 100644 --- a/dotnet/src/webdriver/NoSuchDriverException.cs +++ b/dotnet/src/webdriver/NoSuchDriverException.cs @@ -64,18 +64,6 @@ public NoSuchDriverException(string message, Exception innerException) { } - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected NoSuchDriverException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - /// /// Add information about obtaining additional support from documentation to this exception. /// diff --git a/dotnet/src/webdriver/NoSuchElementException.cs b/dotnet/src/webdriver/NoSuchElementException.cs index ab6150212818b..cfce90ed7e621 100644 --- a/dotnet/src/webdriver/NoSuchElementException.cs +++ b/dotnet/src/webdriver/NoSuchElementException.cs @@ -64,18 +64,6 @@ public NoSuchElementException(string message, Exception innerException) { } - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected NoSuchElementException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - /// /// Add information about obtaining additional support from documentation to this exception. /// diff --git a/dotnet/src/webdriver/NoSuchFrameException.cs b/dotnet/src/webdriver/NoSuchFrameException.cs index 6cd8d57d0894d..a3c98cef0509e 100644 --- a/dotnet/src/webdriver/NoSuchFrameException.cs +++ b/dotnet/src/webdriver/NoSuchFrameException.cs @@ -57,17 +57,5 @@ public NoSuchFrameException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected NoSuchFrameException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/NoSuchShadowRootException.cs b/dotnet/src/webdriver/NoSuchShadowRootException.cs index e008da17ec11b..32398d6aecfd2 100644 --- a/dotnet/src/webdriver/NoSuchShadowRootException.cs +++ b/dotnet/src/webdriver/NoSuchShadowRootException.cs @@ -57,17 +57,5 @@ public NoSuchShadowRootException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected NoSuchShadowRootException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/NoSuchWindowException.cs b/dotnet/src/webdriver/NoSuchWindowException.cs index a9674d45f881a..410d1878e3076 100644 --- a/dotnet/src/webdriver/NoSuchWindowException.cs +++ b/dotnet/src/webdriver/NoSuchWindowException.cs @@ -57,17 +57,5 @@ public NoSuchWindowException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected NoSuchWindowException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/NotFoundException.cs b/dotnet/src/webdriver/NotFoundException.cs index 1316d83f7c2c1..1621ca93d999e 100644 --- a/dotnet/src/webdriver/NotFoundException.cs +++ b/dotnet/src/webdriver/NotFoundException.cs @@ -57,17 +57,5 @@ public NotFoundException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected NotFoundException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/SeleniumManager.cs b/dotnet/src/webdriver/SeleniumManager.cs index 5e365ede075b6..d57b13f278180 100644 --- a/dotnet/src/webdriver/SeleniumManager.cs +++ b/dotnet/src/webdriver/SeleniumManager.cs @@ -24,7 +24,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Text.Json; -using System.Text.Json.Nodes; +using System.Text.Json.Serialization; namespace OpenQA.Selenium { @@ -38,6 +38,8 @@ public static class SeleniumManager private static readonly string BinaryFullPath = Environment.GetEnvironmentVariable("SE_MANAGER_PATH"); + private static readonly JsonSerializerOptions _serializerOptions = new() { PropertyNameCaseInsensitive = true, TypeInfoResolver = SeleniumManagerSerializerContext.Default }; + static SeleniumManager() { @@ -86,10 +88,12 @@ public static Dictionary BinaryPaths(string arguments) argsBuilder.Append(" --debug"); } - var output = RunCommand(BinaryFullPath, argsBuilder.ToString()); - Dictionary binaryPaths = new Dictionary(); - binaryPaths.Add("browser_path", (string)output["browser_path"]); - binaryPaths.Add("driver_path", (string)output["driver_path"]); + var smCommandResult = RunCommand(BinaryFullPath, argsBuilder.ToString()); + Dictionary binaryPaths = new() + { + { "browser_path", smCommandResult.BrowserPath }, + { "driver_path", smCommandResult.DriverPath } + }; if (_logger.IsEnabled(LogEventLevel.Trace)) { @@ -108,7 +112,7 @@ public static Dictionary BinaryPaths(string arguments) /// /// the standard output of the execution. /// - private static JsonNode RunCommand(string fileName, string arguments) + private static SeleniumManagerResponse.ResultResponse RunCommand(string fileName, string arguments) { Process process = new Process(); process.StartInfo.FileName = BinaryFullPath; @@ -174,47 +178,76 @@ private static JsonNode RunCommand(string fileName, string arguments) } string output = outputBuilder.ToString().Trim(); - JsonNode resultJsonNode; + + SeleniumManagerResponse jsonResponse; + try { - var deserializedOutput = JsonSerializer.Deserialize>(output); - resultJsonNode = deserializedOutput["result"]; + jsonResponse = JsonSerializer.Deserialize(output, _serializerOptions); } catch (Exception ex) { throw new WebDriverException($"Error deserializing Selenium Manager's response: {output}", ex); } - if (resultJsonNode["logs"] is not null) + if (jsonResponse.Logs is not null) { - var logs = resultJsonNode["logs"]; - foreach (var entry in logs.AsArray()) + foreach (var entry in jsonResponse.Logs) { - switch (entry.GetPropertyName()) + switch (entry.Level) { case "WARN": if (_logger.IsEnabled(LogEventLevel.Warn)) { - _logger.Warn(entry.GetValue()); + _logger.Warn(entry.Message); } break; case "DEBUG": if (_logger.IsEnabled(LogEventLevel.Debug)) { - _logger.Debug(entry.GetValue()); + _logger.Debug(entry.Message); } break; case "INFO": if (_logger.IsEnabled(LogEventLevel.Info)) { - _logger.Info(entry.GetValue()); + _logger.Info(entry.Message); } break; } } } - return resultJsonNode; + return jsonResponse.Result; + } + } + + internal class SeleniumManagerResponse + { + public IReadOnlyList Logs { get; set; } + + public ResultResponse Result { get; set; } + + public class LogEntryResponse + { + public string Level { get; set; } + + public string Message { get; set; } + } + + public class ResultResponse + { + [JsonPropertyName("driver_path")] + public string DriverPath { get; set; } + + [JsonPropertyName("browser_path")] + public string BrowserPath { get; set; } } } + + [JsonSerializable(typeof(SeleniumManagerResponse))] + internal partial class SeleniumManagerSerializerContext : JsonSerializerContext + { + + } } diff --git a/dotnet/src/webdriver/StaleElementReferenceException.cs b/dotnet/src/webdriver/StaleElementReferenceException.cs index a7c7f68de56f4..81f1fdcd97948 100644 --- a/dotnet/src/webdriver/StaleElementReferenceException.cs +++ b/dotnet/src/webdriver/StaleElementReferenceException.cs @@ -63,18 +63,6 @@ public StaleElementReferenceException(string message, Exception innerException) { } - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected StaleElementReferenceException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - /// /// Add information about obtaining additional support from documentation to this exception. /// diff --git a/dotnet/src/webdriver/UnableToSetCookieException.cs b/dotnet/src/webdriver/UnableToSetCookieException.cs index 40e873d714bd0..6ff0d1ddcf5e8 100644 --- a/dotnet/src/webdriver/UnableToSetCookieException.cs +++ b/dotnet/src/webdriver/UnableToSetCookieException.cs @@ -57,17 +57,5 @@ public UnableToSetCookieException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected UnableToSetCookieException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/UnhandledAlertException.cs b/dotnet/src/webdriver/UnhandledAlertException.cs index fe2b56c6b3eb0..7126a7326728e 100644 --- a/dotnet/src/webdriver/UnhandledAlertException.cs +++ b/dotnet/src/webdriver/UnhandledAlertException.cs @@ -72,18 +72,6 @@ public UnhandledAlertException(string message, Exception innerException) { } - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected UnhandledAlertException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - /// /// Gets the text of the unhandled alert. /// @@ -91,17 +79,5 @@ public string AlertText { get { return this.alertText; } } - - /// - /// Populates a SerializationInfo with the data needed to serialize the target object. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); - } } } diff --git a/dotnet/src/webdriver/WebDriverArgumentException.cs b/dotnet/src/webdriver/WebDriverArgumentException.cs index fd92a38fd8091..dcb16ca2cf38c 100644 --- a/dotnet/src/webdriver/WebDriverArgumentException.cs +++ b/dotnet/src/webdriver/WebDriverArgumentException.cs @@ -57,17 +57,5 @@ public WebDriverArgumentException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected WebDriverArgumentException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/WebDriverException.cs b/dotnet/src/webdriver/WebDriverException.cs index 9eb7af0816241..787f5feed25c4 100644 --- a/dotnet/src/webdriver/WebDriverException.cs +++ b/dotnet/src/webdriver/WebDriverException.cs @@ -67,17 +67,5 @@ public WebDriverException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected WebDriverException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/WebDriverTimeoutException.cs b/dotnet/src/webdriver/WebDriverTimeoutException.cs index caedb65302a33..eecadbb915523 100644 --- a/dotnet/src/webdriver/WebDriverTimeoutException.cs +++ b/dotnet/src/webdriver/WebDriverTimeoutException.cs @@ -57,17 +57,5 @@ public WebDriverTimeoutException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected WebDriverTimeoutException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/src/webdriver/XPathLookupException.cs b/dotnet/src/webdriver/XPathLookupException.cs index 81d47ac896e3f..eca80a9e29aba 100644 --- a/dotnet/src/webdriver/XPathLookupException.cs +++ b/dotnet/src/webdriver/XPathLookupException.cs @@ -57,17 +57,5 @@ public XPathLookupException(string message, Exception innerException) : base(message, innerException) { } - - /// - /// Initializes a new instance of the class with serialized data. - /// - /// The that holds the serialized - /// object data about the exception being thrown. - /// The that contains contextual - /// information about the source or destination. - protected XPathLookupException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } } } diff --git a/dotnet/test/common/DevTools/DevToolsNetworkTest.cs b/dotnet/test/common/DevTools/DevToolsNetworkTest.cs index 36f88c6ebae72..81436eb26178c 100644 --- a/dotnet/test/common/DevTools/DevToolsNetworkTest.cs +++ b/dotnet/test/common/DevTools/DevToolsNetworkTest.cs @@ -203,7 +203,6 @@ await domains.Network.Enable(new CurrentCdpVersion.Network.EnableCommandSettings } [Test] - [IgnorePlatform("Windows", "Not working properly")] [IgnoreBrowser(Selenium.Browser.IE, "IE does not support Chrome DevTools Protocol")] [IgnoreBrowser(Selenium.Browser.Firefox, "Firefox does not support Chrome DevTools Protocol")] [IgnoreBrowser(Selenium.Browser.Safari, "Safari does not support Chrome DevTools Protocol")] @@ -232,7 +231,8 @@ await domains.Network.Enable(new CurrentCdpVersion.Network.EnableCommandSettings var searchResponse = await domains.Network.SearchInResponseBody(new CurrentCdpVersion.Network.SearchInResponseBodyCommandSettings() { RequestId = requestIds[0], - Query = "/", + Query = ".*", + IsRegex = true }); Assert.That(searchResponse.Result.Length > 0); diff --git a/dotnet/test/common/JavascriptEnabledBrowserTest.cs b/dotnet/test/common/JavascriptEnabledBrowserTest.cs index 390f3120ae044..78c5a8515c3d1 100644 --- a/dotnet/test/common/JavascriptEnabledBrowserTest.cs +++ b/dotnet/test/common/JavascriptEnabledBrowserTest.cs @@ -34,6 +34,7 @@ public void DocumentShouldReflectLatestDom() [Test] [IgnoreBrowser(Browser.Chrome, "Not working properly in Chrome")] + [IgnoreBrowser(Browser.Edge, "Not working properly in Edge")] public void ShouldWaitForLoadsToCompleteAfterJavascriptCausesANewPageToLoad() { driver.Url = formsPage; @@ -45,6 +46,7 @@ public void ShouldWaitForLoadsToCompleteAfterJavascriptCausesANewPageToLoad() [Test] [IgnoreBrowser(Browser.Chrome, "Not working properly in Chrome")] + [IgnoreBrowser(Browser.Edge, "Not working properly in Edge")] public void ShouldBeAbleToFindElementAfterJavascriptCausesANewPageToLoad() { driver.Url = formsPage; diff --git a/java/maven_install.json b/java/maven_install.json index 34adb920c307a..8be2dd9b44a41 100644 --- a/java/maven_install.json +++ b/java/maven_install.json @@ -1,7 +1,7 @@ { "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL", - "__INPUT_ARTIFACTS_HASH": -809854593, - "__RESOLVED_ARTIFACTS_HASH": -1445024694, + "__INPUT_ARTIFACTS_HASH": -1369959342, + "__RESOLVED_ARTIFACTS_HASH": 2051378450, "conflict_resolution": { "com.google.code.gson:gson:2.8.9": "com.google.code.gson:gson:2.11.0", "com.google.errorprone:error_prone_annotations:2.3.2": "com.google.errorprone:error_prone_annotations:2.28.0", @@ -194,17 +194,17 @@ }, "com.graphql-java:graphql-java": { "shasums": { - "jar": "98c63c1bf51876f84a3770573279be4f98bbfc2c86d6b4819c327fa1cbd2b137", - "sources": "2f8be51261734b4618551724a5730d1e6299a9adda171178027eacdc8010363a" + "jar": "8828fef5d8133d3d5ad23cee262a9b3ab4ce95aedf5e3332bb577a9aa7c627e0", + "sources": "122d4adc1c1491f86f08f6ba6206aa9f05d14a02cf36c73baad7e1d8034160a7" }, - "version": "20.2" + "version": "22.3" }, "com.graphql-java:java-dataloader": { "shasums": { - "jar": "b9c7d32aef05a2e33dc07c5ce45b713c405b61c6264cb0ed48aac003add3eaa4", - "sources": "ffecf802d587b05860d8ab116d3c6a8630f8eca5d3b5da783f8dd085e2ca0591" + "jar": "08cec84ac76e32b53ea666260f288f10b3731c21c89f9199b109ced2361f78b8", + "sources": "cea71f74025c2ca95618113345f888c780a99bf3133621b54007e1babaef2e14" }, - "version": "3.2.0" + "version": "3.3.0" }, "commons-codec:commons-codec": { "shasums": { @@ -900,8 +900,7 @@ ], "com.graphql-java:graphql-java": [ "com.graphql-java:java-dataloader", - "org.reactivestreams:reactive-streams", - "org.slf4j:slf4j-api" + "org.reactivestreams:reactive-streams" ], "com.graphql-java:java-dataloader": [ "org.slf4j:slf4j-api" @@ -1425,16 +1424,18 @@ ], "com.graphql-java:graphql-java": [ "graphql", + "graphql.agent.result", "graphql.analysis", "graphql.analysis.values", - "graphql.cachecontrol", "graphql.collect", "graphql.com.google.common.base", "graphql.com.google.common.collect", "graphql.com.google.common.math", "graphql.com.google.common.primitives", "graphql.execution", + "graphql.execution.conditional", "graphql.execution.directives", + "graphql.execution.incremental", "graphql.execution.instrumentation", "graphql.execution.instrumentation.dataloader", "graphql.execution.instrumentation.fieldvalidation", @@ -1444,11 +1445,15 @@ "graphql.execution.preparsed", "graphql.execution.preparsed.persisted", "graphql.execution.reactive", + "graphql.execution.values", + "graphql.execution.values.legacycoercing", "graphql.extensions", "graphql.i18n", + "graphql.incremental", "graphql.introspection", "graphql.language", "graphql.normalized", + "graphql.normalized.incremental", "graphql.org.antlr.v4.runtime", "graphql.org.antlr.v4.runtime.atn", "graphql.org.antlr.v4.runtime.dfa", @@ -1466,7 +1471,6 @@ "graphql.schema.diff.reporting", "graphql.schema.diffing", "graphql.schema.diffing.ana", - "graphql.schema.diffing.dot", "graphql.schema.fetching", "graphql.schema.idl", "graphql.schema.idl.errors", @@ -1475,6 +1479,7 @@ "graphql.schema.usage", "graphql.schema.validation", "graphql.schema.visibility", + "graphql.schema.visitor", "graphql.util", "graphql.validation", "graphql.validation.rules" @@ -1484,6 +1489,7 @@ "org.dataloader.annotations", "org.dataloader.impl", "org.dataloader.registries", + "org.dataloader.scheduler", "org.dataloader.stats", "org.dataloader.stats.context" ], diff --git a/java/src/org/openqa/selenium/grid/graphql/BUILD.bazel b/java/src/org/openqa/selenium/grid/graphql/BUILD.bazel index 7c008441e8699..4fc0d84d5aa70 100644 --- a/java/src/org/openqa/selenium/grid/graphql/BUILD.bazel +++ b/java/src/org/openqa/selenium/grid/graphql/BUILD.bazel @@ -21,6 +21,5 @@ java_library( "//java/src/org/openqa/selenium/remote/http", artifact("com.google.guava:guava"), artifact("com.graphql-java:graphql-java"), - artifact("com.graphql-java:java-dataloader"), ], ) diff --git a/java/src/org/openqa/selenium/grid/graphql/GraphqlHandler.java b/java/src/org/openqa/selenium/grid/graphql/GraphqlHandler.java index 28f35c477cbbe..3ab7a7385f701 100644 --- a/java/src/org/openqa/selenium/grid/graphql/GraphqlHandler.java +++ b/java/src/org/openqa/selenium/grid/graphql/GraphqlHandler.java @@ -45,6 +45,7 @@ import java.net.URI; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import org.openqa.selenium.grid.distributor.Distributor; import org.openqa.selenium.grid.sessionqueue.NewSessionQueue; @@ -88,7 +89,7 @@ public GraphqlHandler( new SchemaGenerator() .makeExecutableSchema(buildTypeDefinitionRegistry(), buildRuntimeWiring()); - Cache cache = + Cache> cache = CacheBuilder.newBuilder().maximumSize(1024).build(); graphQl = @@ -97,7 +98,10 @@ public GraphqlHandler( (executionInput, computeFunction) -> { try { return cache.get( - executionInput.getQuery(), () -> computeFunction.apply(executionInput)); + executionInput.getQuery(), + () -> + CompletableFuture.supplyAsync( + () -> computeFunction.apply(executionInput))); } catch (ExecutionException e) { if (e.getCause() instanceof RuntimeException) { throw (RuntimeException) e.getCause(); diff --git a/java/src/org/openqa/selenium/grid/node/config/NodeFlags.java b/java/src/org/openqa/selenium/grid/node/config/NodeFlags.java index 3d49df5f713b3..800a0798a4e17 100644 --- a/java/src/org/openqa/selenium/grid/node/config/NodeFlags.java +++ b/java/src/org/openqa/selenium/grid/node/config/NodeFlags.java @@ -30,7 +30,7 @@ import static org.openqa.selenium.grid.node.config.NodeOptions.DEFAULT_REGISTER_PERIOD; import static org.openqa.selenium.grid.node.config.NodeOptions.DEFAULT_SESSION_TIMEOUT; import static org.openqa.selenium.grid.node.config.NodeOptions.DEFAULT_USE_SELENIUM_MANAGER; -import static org.openqa.selenium.grid.node.config.NodeOptions.DEFAULT_VNC_ENV_VAR; +import static org.openqa.selenium.grid.node.config.NodeOptions.DEFAULT_VNC_ENV_VARS; import static org.openqa.selenium.grid.node.config.NodeOptions.NODE_SECTION; import static org.openqa.selenium.grid.node.config.NodeOptions.OVERRIDE_MAX_SESSIONS; @@ -202,8 +202,11 @@ public class NodeFlags implements HasRoles { description = "Environment variable to check in order to determine if a vnc stream is " + "available or not.") - @ConfigValue(section = NODE_SECTION, name = "vnc-env-var", example = "SE_START_XVFB") - public String vncEnvVar = DEFAULT_VNC_ENV_VAR; + @ConfigValue( + section = NODE_SECTION, + name = "vnc-env-var", + example = "[\"SE_START_XVFB\", \"SE_START_VNC\", \"SE_START_NO_VNC\"]") + public List vncEnvVar = DEFAULT_VNC_ENV_VARS; @Parameter( names = "--no-vnc-port", diff --git a/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java b/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java index 8f7ea2dd68aaa..7317f6e7c8870 100644 --- a/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java +++ b/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java @@ -31,6 +31,7 @@ import java.net.URISyntaxException; import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; @@ -78,7 +79,8 @@ public class NodeOptions { static final boolean DEFAULT_DETECT_DRIVERS = true; static final boolean DEFAULT_USE_SELENIUM_MANAGER = false; static final boolean OVERRIDE_MAX_SESSIONS = false; - static final String DEFAULT_VNC_ENV_VAR = "SE_START_XVFB"; + static final List DEFAULT_VNC_ENV_VARS = + Arrays.asList("SE_START_XVFB", "SE_START_VNC", "SE_START_NO_VNC"); static final int DEFAULT_NO_VNC_PORT = 7900; static final int DEFAULT_REGISTER_CYCLE = 10; static final int DEFAULT_REGISTER_PERIOD = 120; @@ -286,9 +288,16 @@ public int getDrainAfterSessionCount() { @VisibleForTesting boolean isVncEnabled() { - String vncEnvVar = config.get(NODE_SECTION, "vnc-env-var").orElse(DEFAULT_VNC_ENV_VAR); + List vncEnvVars = DEFAULT_VNC_ENV_VARS; + if (config.getAll(NODE_SECTION, "vnc-env-var").isPresent()) { + vncEnvVars = config.getAll(NODE_SECTION, "vnc-env-var").get(); + } if (!vncEnabledValueSet.getAndSet(true)) { - vncEnabled.set(Boolean.parseBoolean(System.getenv(vncEnvVar))); + boolean allEnabled = + vncEnvVars.stream() + .allMatch( + env -> "true".equalsIgnoreCase(System.getProperty(env, System.getenv(env)))); + vncEnabled.set(allEnabled); } return vncEnabled.get(); } diff --git a/java/src/org/openqa/selenium/os/ExternalProcess.java b/java/src/org/openqa/selenium/os/ExternalProcess.java index b9c05f441a347..2bbb7ceb01b9c 100644 --- a/java/src/org/openqa/selenium/os/ExternalProcess.java +++ b/java/src/org/openqa/selenium/os/ExternalProcess.java @@ -317,8 +317,11 @@ public void shutdown() { */ public void shutdown(Duration timeout) { try { - if (process.supportsNormalTermination()) { - process.destroy(); + // use the handle to prevent closing the stdin, stdout, stderr streams + ProcessHandle handle = process.toHandle(); + + if (handle.supportsNormalTermination()) { + handle.destroy(); try { if (process.waitFor(timeout.toMillis(), MILLISECONDS)) { @@ -330,7 +333,7 @@ public void shutdown(Duration timeout) { } } - process.destroyForcibly(); + handle.destroyForcibly(); try { process.waitFor(timeout.toMillis(), MILLISECONDS); } catch (InterruptedException ex) { diff --git a/java/src/org/openqa/selenium/remote/codec/AbstractHttpResponseCodec.java b/java/src/org/openqa/selenium/remote/codec/AbstractHttpResponseCodec.java index b806dfc40935c..78b18f76e09d3 100644 --- a/java/src/org/openqa/selenium/remote/codec/AbstractHttpResponseCodec.java +++ b/java/src/org/openqa/selenium/remote/codec/AbstractHttpResponseCodec.java @@ -108,13 +108,6 @@ public Response decode(HttpResponse encodedResponse) { response.setValue(content); } - if (response.getValue() instanceof String) { - // We normalise to \n because Java will translate this to \r\n - // if this is suitable on our platform, and if we have \r\n, java will - // turn this into \r\r\n, which would be Bad! - response.setValue(((String) response.getValue()).replace("\r\n", "\n")); - } - if (response.getStatus() != null && response.getState() == null) { response.setState(errorCodes.toState(response.getStatus())); } else if (response.getStatus() == null && response.getState() != null) { diff --git a/java/src/org/openqa/selenium/remote/service/DriverFinder.java b/java/src/org/openqa/selenium/remote/service/DriverFinder.java index 7216b5d9783d1..436878fbbb48f 100644 --- a/java/src/org/openqa/selenium/remote/service/DriverFinder.java +++ b/java/src/org/openqa/selenium/remote/service/DriverFinder.java @@ -156,6 +156,8 @@ private List toArguments() { arguments.add(proxy.getSslProxy()); } else if (proxy.getHttpProxy() != null) { arguments.add(proxy.getHttpProxy()); + } else if (proxy.getProxyAutoconfigUrl() != null) { + arguments.add(proxy.getProxyAutoconfigUrl()); } } return arguments; diff --git a/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java b/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java index 43d413cdab362..69f1ba30a9ee9 100644 --- a/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java +++ b/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java @@ -719,6 +719,50 @@ void settingSlotMatcherAvailable() { assertThat(nodeOptions.getSlotMatcher()).isExactlyInstanceOf(YesSlotMatcher.class); } + @Test + void testIsVncEnabledAcceptListEnvVarsAndReturnTrue() { + System.setProperty("SE_START_XVFB", "true"); + System.setProperty("SE_START_VNC", "true"); + System.setProperty("SE_START_NO_VNC", "true"); + String[] rawConfig = + new String[] { + "[node]", "vnc-env-var = [\"SE_START_XVFB\", \"SE_START_VNC\", \"SE_START_NO_VNC\"]", + }; + Config config = new TomlConfig(new StringReader(String.join("\n", rawConfig))); + NodeOptions nodeOptionsEnabled = new NodeOptions(config); + assertThat(config.getAll("node", "vnc-env-var").get()) + .containsExactly("SE_START_XVFB", "SE_START_VNC", "SE_START_NO_VNC"); + assertThat(nodeOptionsEnabled.isVncEnabled()).isTrue(); + } + + @Test + void testIsVncEnabledAcceptListEnvVarsAndReturnFalse() { + System.setProperty("SE_START_XVFB", "true"); + System.setProperty("SE_START_VNC", "false"); + String[] rawConfig = + new String[] { + "[node]", "vnc-env-var = [\"SE_START_XVFB\", \"SE_START_VNC\", \"SE_START_NO_VNC\"]", + }; + Config config = new TomlConfig(new StringReader(String.join("\n", rawConfig))); + NodeOptions nodeOptionsEnabled = new NodeOptions(config); + assertThat(config.getAll("node", "vnc-env-var").get()) + .containsExactly("SE_START_XVFB", "SE_START_VNC", "SE_START_NO_VNC"); + assertThat(nodeOptionsEnabled.isVncEnabled()).isFalse(); + } + + @Test + void testIsVncEnabledAcceptSingleEnvVar() { + System.setProperty("SE_START_XVFB", "false"); + String[] rawConfig = + new String[] { + "[node]", "vnc-env-var = \"SE_START_XVFB\"", + }; + Config config = new TomlConfig(new StringReader(String.join("\n", rawConfig))); + NodeOptions nodeOptionsEnabled = new NodeOptions(config); + assertThat(config.getAll("node", "vnc-env-var").get()).containsExactly("SE_START_XVFB"); + assertThat(nodeOptionsEnabled.isVncEnabled()).isFalse(); + } + private Condition> supporting(String name) { return new Condition<>( caps -> caps.stream().anyMatch(cap -> name.equals(cap.getBrowserName())), diff --git a/javascript/grid-ui/src/components/Node/Node.tsx b/javascript/grid-ui/src/components/Node/Node.tsx index f61b2ec550288..79b2ced05f2a9 100644 --- a/javascript/grid-ui/src/components/Node/Node.tsx +++ b/javascript/grid-ui/src/components/Node/Node.tsx @@ -24,29 +24,28 @@ import OsLogo from '../common/OsLogo' function Node (props) { const { node } = props - const nodeStatusDown = node.status === 'DOWN' + const getCardStyle = (status: string) => ({ + height: '100%', + flexGrow: 1, + opacity: status === 'DOWN' ? 0.25 : 1, + bgcolor: (status === 'DOWN' || status === 'DRAINING') ? 'grey.A100' : '' + }) return ( - + - + URI: {node.uri} @@ -54,19 +53,19 @@ function Node (props) { - - + + - + - + diff --git a/javascript/grid-ui/src/screens/Overview/Overview.tsx b/javascript/grid-ui/src/screens/Overview/Overview.tsx index c51b10140305a..4ff654adad294 100644 --- a/javascript/grid-ui/src/screens/Overview/Overview.tsx +++ b/javascript/grid-ui/src/screens/Overview/Overview.tsx @@ -19,6 +19,16 @@ import Grid from '@mui/material/Grid' import Paper from '@mui/material/Paper' import { loader } from 'graphql.macro' import React from 'react' +import { useState, useEffect, useMemo } from 'react' +import { + Box, + Checkbox, + FormControl, + FormControlLabel, + InputLabel, + MenuItem, + Select +} from '@mui/material' import Node from '../../components/Node/Node' import { useQuery } from '@apollo/client' import NodeInfo from '../../models/node-info' @@ -30,7 +40,7 @@ import StereotypeInfo from '../../models/stereotype-info' import browserVersion from '../../util/browser-version' import Capabilities from '../../models/capabilities' import { GridConfig } from '../../config' -import {NODES_QUERY} from "../../graphql/nodes"; +import { NODES_QUERY } from '../../graphql/nodes' function Overview (): JSX.Element { const { loading, error, data } = useQuery(NODES_QUERY, { @@ -38,12 +48,85 @@ function Overview (): JSX.Element { fetchPolicy: 'network-only' }) + const [sortOption, setSortOption] = useState('osInfo.name') + const [sortOrder, setSortOrder] = useState(1) + const [sortedNodes, setSortedNodes] = useState([]) + const [isDescending, setIsDescending] = useState(false) + + const handleSortChange = (event: React.ChangeEvent<{ value: unknown }>) => { + setSortOption(event.target.value as string) + } + + const handleOrderChange = (event: React.ChangeEvent) => { + setIsDescending(event.target.checked) + setSortOrder(event.target.checked ? -1 : 1) + } + + const sortProperties = { + 'osInfo.name': (a, b) => a.osInfo.name.localeCompare(b.osInfo.name), + 'status': (a, b) => a.status.localeCompare(b.status), + 'id': (a, b) => (a.id < b.id ? -1 : 1) + } + + const sortNodes = useMemo(() => { + return (nodes: NodeInfo[], option: string, order: number) => { + const sortFn = sortProperties[option] || (() => 0) + return nodes.sort((a, b) => order * sortFn(a, b)) + } + }, [sortOption, sortOrder]) + + useEffect(() => { + if (data) { + const unSortedNodes = data.nodesInfo.nodes.map((node) => { + const osInfo: OsInfo = { + name: node.osInfo.name, + version: node.osInfo.version, + arch: node.osInfo.arch + } + + interface StereoTypeData { + stereotype: Capabilities; + slots: number; + } + + const slotStereotypes = (JSON.parse( + node.stereotypes) as StereoTypeData[]).map((item) => { + const slotStereotype: StereotypeInfo = { + browserName: item.stereotype.browserName ?? '', + browserVersion: browserVersion( + item.stereotype.browserVersion ?? item.stereotype.version), + platformName: (item.stereotype.platformName + ?? item.stereotype.platform) ?? '', + slotCount: item.slots, + rawData: item + } + return slotStereotype + }) + + const newNode: NodeInfo = { + uri: node.uri, + id: node.id, + status: node.status, + maxSession: node.maxSession, + slotCount: node.slotCount, + version: node.version, + osInfo: osInfo, + sessionCount: node.sessionCount ?? 0, + slotStereotypes: slotStereotypes + } + return newNode + }) + + setSortedNodes(sortNodes(unSortedNodes, sortOption, sortOrder)) + } + }, [data, sortOption, sortOrder]) + if (error !== undefined) { const message = 'There has been an error while loading the Nodes from the Grid.' const errorMessage = error?.networkError?.message return ( - + ) } @@ -51,64 +134,43 @@ function Overview (): JSX.Element { if (loading) { return ( - + ) } - const unSortedNodes = data.nodesInfo.nodes.map((node) => { - const osInfo: OsInfo = { - name: node.osInfo.name, - version: node.osInfo.version, - arch: node.osInfo.arch - } - - interface StereoTypeData { - stereotype: Capabilities - slots: number - } - - const slotStereotypes = (JSON.parse( - node.stereotypes) as StereoTypeData[]).map((item) => { - const slotStereotype: StereotypeInfo = { - browserName: item.stereotype.browserName ?? '', - browserVersion: browserVersion( - item.stereotype.browserVersion ?? item.stereotype.version), - platformName: (item.stereotype.platformName ?? - item.stereotype.platform) ?? '', - slotCount: item.slots, - rawData: item - } - return slotStereotype - }) - const newNode: NodeInfo = { - uri: node.uri, - id: node.id, - status: node.status, - maxSession: node.maxSession, - slotCount: node.slotCount, - version: node.version, - osInfo: osInfo, - sessionCount: node.sessionCount ?? 0, - slotStereotypes: slotStereotypes - } - return newNode - }) - - const nodes = unSortedNodes.sort((a, b) => (a.id < b.id ? -1 : 1)) - if (nodes.length === 0) { + if (sortedNodes.length === 0) { const shortMessage = 'The Grid has no registered Nodes yet.' return ( - + ) } return ( - {/* Nodes */} - {nodes.map((node, index) => { + + + Sort By + + + } + label="Descending" + style={{ marginLeft: '8px' }} + /> + + + + {sortedNodes.map((node, index) => { return ( - + ) diff --git a/py/selenium/webdriver/chromium/service.py b/py/selenium/webdriver/chromium/service.py index fc7d165f2b8f0..aebedec40f509 100644 --- a/py/selenium/webdriver/chromium/service.py +++ b/py/selenium/webdriver/chromium/service.py @@ -39,9 +39,11 @@ def __init__( service_args: typing.Optional[typing.List[str]] = None, log_output: SubprocessStdAlias = None, env: typing.Optional[typing.Mapping[str, str]] = None, + driver_path_env_key: str = None, **kwargs, ) -> None: self.service_args = service_args or [] + driver_path_env_key = driver_path_env_key or "SE_CHROMEDRIVER" if isinstance(log_output, str): self.service_args.append(f"--log-path={log_output}") @@ -56,6 +58,7 @@ def __init__( port=port, env=env, log_output=self.log_output, + driver_path_env_key=driver_path_env_key, **kwargs, ) diff --git a/py/selenium/webdriver/chromium/webdriver.py b/py/selenium/webdriver/chromium/webdriver.py index 6fc3adeb4086d..d7bf5c706a453 100644 --- a/py/selenium/webdriver/chromium/webdriver.py +++ b/py/selenium/webdriver/chromium/webdriver.py @@ -51,7 +51,7 @@ def __init__( options.binary_location = finder.get_browser_path() options.browser_version = None - self.service.path = finder.get_driver_path() + self.service.path = self.service.env_path() or finder.get_driver_path() self.service.start() executor = ChromiumRemoteConnection( diff --git a/py/selenium/webdriver/common/bidi/script.py b/py/selenium/webdriver/common/bidi/script.py index 88a26b6437ca2..6819a5cf63436 100644 --- a/py/selenium/webdriver/common/bidi/script.py +++ b/py/selenium/webdriver/common/bidi/script.py @@ -64,7 +64,6 @@ class LogEntryAdded: @classmethod def from_json(cls, json): - print(json) if json["type"] == "console": return ConsoleLogEntry.from_json(json) elif json["type"] == "javascript": diff --git a/py/selenium/webdriver/common/service.py b/py/selenium/webdriver/common/service.py index 829e4f43ad967..3afbced1a3e88 100644 --- a/py/selenium/webdriver/common/service.py +++ b/py/selenium/webdriver/common/service.py @@ -25,6 +25,7 @@ from platform import system from subprocess import PIPE from time import sleep +from typing import Optional from typing import cast from urllib import request from urllib.error import URLError @@ -53,6 +54,7 @@ def __init__( port: int = 0, log_output: SubprocessStdAlias = None, env: typing.Optional[typing.Mapping[typing.Any, typing.Any]] = None, + driver_path_env_key: str = None, **kwargs, ) -> None: if isinstance(log_output, str): @@ -64,12 +66,13 @@ def __init__( else: self.log_output = log_output - self._path = executable_path self.port = port or utils.free_port() # Default value for every python subprocess: subprocess.Popen(..., creationflags=0) self.popen_kw = kwargs.pop("popen_kw", {}) self.creation_flags = self.popen_kw.pop("creation_flags", 0) self.env = env or os.environ + self.DRIVER_PATH_ENV_KEY = driver_path_env_key + self._path = self.env_path() or executable_path @property def service_url(self) -> str: @@ -236,3 +239,6 @@ def _start_process(self, path: str) -> None: f"'{os.path.basename(self._path)}' executable may have wrong permissions." ) from err raise + + def env_path(self) -> Optional[str]: + return os.getenv(self.DRIVER_PATH_ENV_KEY, None) diff --git a/py/selenium/webdriver/edge/service.py b/py/selenium/webdriver/edge/service.py index 7a9142ce75467..b2c7f02faf4f7 100644 --- a/py/selenium/webdriver/edge/service.py +++ b/py/selenium/webdriver/edge/service.py @@ -26,8 +26,6 @@ class Service(service.ChromiumService): :param executable_path: install path of the msedgedriver executable, defaults to `msedgedriver`. :param port: Port for the service to run on, defaults to 0 where the operating system will decide. - :param verbose: (Deprecated) Whether to make the webdriver more verbose (passes the --verbose option to the binary). - Defaults to False. :param log_output: (Optional) int representation of STDOUT/DEVNULL, any IO instance or String path to file. :param service_args: (Optional) List of args to be passed to the subprocess when launching the executable. :param env: (Optional) Mapping of environment variables for the new process, defaults to `os.environ`. @@ -40,9 +38,11 @@ def __init__( log_output: SubprocessStdAlias = None, service_args: typing.Optional[typing.List[str]] = None, env: typing.Optional[typing.Mapping[str, str]] = None, + driver_path_env_key: str = None, **kwargs, ) -> None: self.service_args = service_args or [] + driver_path_env_key = driver_path_env_key or "SE_EDGEDRIVER" super().__init__( executable_path=executable_path, @@ -50,5 +50,6 @@ def __init__( service_args=service_args, log_output=log_output, env=env, + driver_path_env_key=driver_path_env_key, **kwargs, ) diff --git a/py/selenium/webdriver/firefox/service.py b/py/selenium/webdriver/firefox/service.py index 4b25cc7b5304d..e34431480547e 100644 --- a/py/selenium/webdriver/firefox/service.py +++ b/py/selenium/webdriver/firefox/service.py @@ -40,15 +40,18 @@ def __init__( service_args: typing.Optional[typing.List[str]] = None, log_output: SubprocessStdAlias = None, env: typing.Optional[typing.Mapping[str, str]] = None, + driver_path_env_key: str = None, **kwargs, ) -> None: self.service_args = service_args or [] + driver_path_env_key = driver_path_env_key or "SE_GECKODRIVER" super().__init__( executable_path=executable_path, port=port, log_output=log_output, env=env, + driver_path_env_key=driver_path_env_key, **kwargs, ) diff --git a/py/selenium/webdriver/firefox/webdriver.py b/py/selenium/webdriver/firefox/webdriver.py index c04c2d4a7b458..dd686d93923cc 100644 --- a/py/selenium/webdriver/firefox/webdriver.py +++ b/py/selenium/webdriver/firefox/webdriver.py @@ -58,7 +58,7 @@ def __init__( options.binary_location = finder.get_browser_path() options.browser_version = None - self.service.path = finder.get_driver_path() + self.service.path = self.service.env_path() or finder.get_driver_path() self.service.start() executor = FirefoxRemoteConnection( diff --git a/py/selenium/webdriver/ie/service.py b/py/selenium/webdriver/ie/service.py index 4b0d7f0f3bd90..b8fac381d9f35 100644 --- a/py/selenium/webdriver/ie/service.py +++ b/py/selenium/webdriver/ie/service.py @@ -32,6 +32,7 @@ def __init__( service_args: typing.Optional[typing.List[str]] = None, log_level: typing.Optional[str] = None, log_output: SubprocessStdAlias = None, + driver_path_env_key: str = None, **kwargs, ) -> None: """Creates a new instance of the Service. @@ -46,6 +47,8 @@ def __init__( Default is "stdout". """ self.service_args = service_args or [] + driver_path_env_key = driver_path_env_key or "SE_IEDRIVER" + if host: self.service_args.append(f"--host={host}") if log_level: @@ -55,6 +58,7 @@ def __init__( executable_path=executable_path, port=port, log_output=log_output, + driver_path_env_key=driver_path_env_key, **kwargs, ) diff --git a/py/selenium/webdriver/ie/webdriver.py b/py/selenium/webdriver/ie/webdriver.py index 64bf79fe250dc..11c137e509fe7 100644 --- a/py/selenium/webdriver/ie/webdriver.py +++ b/py/selenium/webdriver/ie/webdriver.py @@ -46,7 +46,7 @@ def __init__( self.service = service if service else Service() options = options if options else Options() - self.service.path = DriverFinder(self.service, options).get_driver_path() + self.service.path = self.service.env_path() or DriverFinder(self.service, options).get_driver_path() self.service.start() executor = RemoteConnection( diff --git a/py/selenium/webdriver/safari/service.py b/py/selenium/webdriver/safari/service.py index 9728b877720c4..3386198c893ac 100644 --- a/py/selenium/webdriver/safari/service.py +++ b/py/selenium/webdriver/safari/service.py @@ -37,15 +37,18 @@ def __init__( service_args: typing.Optional[typing.List[str]] = None, env: typing.Optional[typing.Mapping[str, str]] = None, reuse_service=False, + driver_path_env_key: str = None, **kwargs, ) -> None: self.service_args = service_args or [] + driver_path_env_key = driver_path_env_key or "SE_SAFARIDRIVER" self.reuse_service = reuse_service super().__init__( executable_path=executable_path, port=port, env=env, + driver_path_env_key=driver_path_env_key, **kwargs, ) diff --git a/py/selenium/webdriver/safari/webdriver.py b/py/selenium/webdriver/safari/webdriver.py index 259b2d82047bf..2c37a4bd7abde 100644 --- a/py/selenium/webdriver/safari/webdriver.py +++ b/py/selenium/webdriver/safari/webdriver.py @@ -45,7 +45,7 @@ def __init__( self.service = service if service else Service() options = options if options else Options() - self.service.path = DriverFinder(self.service, options).get_driver_path() + self.service.path = self.service.env_path() or DriverFinder(self.service, options).get_driver_path() if not self.service.reuse_service: self.service.start() diff --git a/py/test/selenium/webdriver/chrome/chrome_service_tests.py b/py/test/selenium/webdriver/chrome/chrome_service_tests.py index 51c0252141773..c3b3360c939c4 100644 --- a/py/test/selenium/webdriver/chrome/chrome_service_tests.py +++ b/py/test/selenium/webdriver/chrome/chrome_service_tests.py @@ -95,3 +95,29 @@ def test_log_output_null_default(driver, capfd) -> None: out, err = capfd.readouterr() assert "Starting ChromeDriver" not in out driver.quit() + + +@pytest.fixture +def service(): + return Service() + + +@pytest.mark.usefixtures("service") +class TestChromeDriverService: + service_path = "/path/to/chromedriver" + + @pytest.fixture(autouse=True) + def setup_and_teardown(self): + os.environ["SE_CHROMEDRIVER"] = self.service_path + yield + os.environ.pop("SE_CHROMEDRIVER", None) + + def test_uses_path_from_env_variable(self, service): + assert "chromedriver" in service.path + + def test_updates_path_after_setting_env_variable(self, service): + new_path = "/foo/bar" + os.environ["SE_CHROMEDRIVER"] = new_path + service.executable_path = self.service_path # Simulating the update + + assert "chromedriver" in service.executable_path diff --git a/py/test/selenium/webdriver/common/position_and_size_tests.py b/py/test/selenium/webdriver/common/position_and_size_tests.py index 7ef1334a7a0f3..a438c0f5e0c88 100644 --- a/py/test/selenium/webdriver/common/position_and_size_tests.py +++ b/py/test/selenium/webdriver/common/position_and_size_tests.py @@ -109,12 +109,6 @@ def test_should_correctly_identify_that_an_element_has_width_and_height(driver, def _check_location(location, **kwargs): - try: - # python 2.x - expected = kwargs.viewitems() - actual = location.viewitems() - except AttributeError: - # python 3.x - expected = kwargs.items() - actual = location.items() + expected = kwargs.items() + actual = location.items() assert expected <= actual diff --git a/py/test/selenium/webdriver/firefox/firefox_service_tests.py b/py/test/selenium/webdriver/firefox/firefox_service_tests.py index dde881c8141b9..2a57d0341acee 100644 --- a/py/test/selenium/webdriver/firefox/firefox_service_tests.py +++ b/py/test/selenium/webdriver/firefox/firefox_service_tests.py @@ -17,6 +17,8 @@ import os import subprocess +import pytest + from selenium.webdriver import Firefox from selenium.webdriver.firefox.service import Service @@ -54,3 +56,29 @@ def test_log_output_as_stdout(capfd) -> None: out, err = capfd.readouterr() assert "geckodriver\tINFO\tListening" in out driver.quit() + + +@pytest.fixture +def service(): + return Service() + + +@pytest.mark.usefixtures("service") +class TestGeckoDriverService: + service_path = "/path/to/geckodriver" + + @pytest.fixture(autouse=True) + def setup_and_teardown(self): + os.environ["SE_GECKODRIVER"] = self.service_path + yield + os.environ.pop("SE_GECKODRIVER", None) + + def test_uses_path_from_env_variable(self, service): + assert "geckodriver" in service.path + + def test_updates_path_after_setting_env_variable(self, service): + new_path = "/foo/bar" + os.environ["SE_GECKODRIVER"] = new_path + service.executable_path = self.service_path # Simulating the update + + assert "geckodriver" in service.executable_path diff --git a/rust/src/lib.rs b/rust/src/lib.rs index ae050adc125c8..751f092029e6d 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -750,7 +750,10 @@ pub trait SeleniumManager { // Download browser if necessary match self.download_browser_if_necessary(&original_browser_version) { Ok(_) => {} - Err(err) => self.check_error_with_driver_in_path(&use_driver_in_path, err)?, + Err(err) => { + self.set_fallback_driver_from_cache(false); + self.check_error_with_driver_in_path(&use_driver_in_path, err)? + } } // With the discovered browser version, discover the proper driver version using online endpoints