From 6db235f57df9049a5c6ef89b73f7ef91dd641744 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Tue, 7 Jul 2026 11:19:35 +0530 Subject: [PATCH 1/8] [java] Move BiDi creation to RemoteWebDriver. Add enable BiDi to SafariOptions. --- .../selenium/chromium/ChromiumDriver.java | 47 --------------- .../selenium/firefox/FirefoxDriver.java | 59 +------------------ .../selenium/remote/RemoteWebDriver.java | 49 +++++++++++++-- .../openqa/selenium/safari/SafariOptions.java | 17 ++++++ 4 files changed, 62 insertions(+), 110 deletions(-) diff --git a/java/src/org/openqa/selenium/chromium/ChromiumDriver.java b/java/src/org/openqa/selenium/chromium/ChromiumDriver.java index 46ae2d48cb21c..178143445c58e 100644 --- a/java/src/org/openqa/selenium/chromium/ChromiumDriver.java +++ b/java/src/org/openqa/selenium/chromium/ChromiumDriver.java @@ -23,7 +23,6 @@ import static org.openqa.selenium.remote.Browser.OPERA; import java.net.URI; -import java.net.URISyntaxException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -44,8 +43,6 @@ import org.openqa.selenium.ScriptKey; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.bidi.BiDi; -import org.openqa.selenium.bidi.BiDiException; import org.openqa.selenium.bidi.HasBiDi; import org.openqa.selenium.devtools.CdpEndpointFinder; import org.openqa.selenium.devtools.CdpInfo; @@ -87,7 +84,6 @@ public class ChromiumDriver extends RemoteWebDriver private final HasPermissions permissions; private final HasLaunchApp launch; private final Optional devTools; - private final Optional biDi; /** * May be null when the driver does not support casting; initialized during setup if available. @@ -118,22 +114,6 @@ protected ChromiumDriver( HttpClient.Factory factory = HttpClient.Factory.createDefault(); Capabilities originalCapabilities = super.getCapabilities(); - Optional webSocketUrl = - Optional.ofNullable((String) originalCapabilities.getCapability("webSocketUrl")); - - Optional biDiUri = - webSocketUrl.map( - uri -> { - try { - return new URI(uri); - } catch (URISyntaxException e) { - LOG.warning(e.getMessage()); - } - return null; - }); - - this.biDi = createBiDi(biDiUri, clientConfig); - Optional reportedUri = CdpEndpointFinder.getReportedUri(capabilityKey, originalCapabilities); Optional client = @@ -296,33 +276,6 @@ public Optional maybeGetDevTools() { return devTools; } - private Optional createBiDi(Optional biDiUri, ClientConfig clientConfig) { - if (biDiUri.isEmpty()) { - return Optional.empty(); - } - - URI wsUri = - biDiUri.orElseThrow( - () -> - new BiDiException( - "Check if this browser version supports BiDi and if the 'webSocketUrl: true'" - + " capability is set.")); - - HttpClient.Factory clientFactory = HttpClient.Factory.createDefault(); - ClientConfig wsConfig = clientConfig.baseUri(wsUri); - HttpClient wsClient = clientFactory.createClient(wsConfig); - - org.openqa.selenium.bidi.Connection biDiConnection = - new org.openqa.selenium.bidi.Connection(wsClient, wsUri.toString()); - - return Optional.of(new BiDi(biDiConnection, wsConfig.wsTimeout())); - } - - @Override - public Optional maybeGetBiDi() { - return biDi; - } - @Override public List> getCastSinks() { if (this.casting == null) { diff --git a/java/src/org/openqa/selenium/firefox/FirefoxDriver.java b/java/src/org/openqa/selenium/firefox/FirefoxDriver.java index 2546a8556e971..e4166404a0347 100644 --- a/java/src/org/openqa/selenium/firefox/FirefoxDriver.java +++ b/java/src/org/openqa/selenium/firefox/FirefoxDriver.java @@ -19,11 +19,8 @@ import static org.openqa.selenium.remote.CapabilityType.PROXY; -import java.net.URI; -import java.net.URISyntaxException; import java.nio.file.Path; import java.util.Map; -import java.util.Optional; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -34,8 +31,6 @@ import org.openqa.selenium.OutputType; import org.openqa.selenium.Proxy; import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.bidi.BiDi; -import org.openqa.selenium.bidi.BiDiException; import org.openqa.selenium.bidi.HasBiDi; import org.openqa.selenium.internal.Require; import org.openqa.selenium.remote.CommandInfo; @@ -43,7 +38,6 @@ import org.openqa.selenium.remote.RemoteWebDriver; import org.openqa.selenium.remote.RemoteWebDriverBuilder; import org.openqa.selenium.remote.http.ClientConfig; -import org.openqa.selenium.remote.http.HttpClient; import org.openqa.selenium.remote.service.DriverCommandExecutor; import org.openqa.selenium.remote.service.DriverFinder; import org.openqa.selenium.remote.service.DriverService; @@ -71,8 +65,6 @@ public class FirefoxDriver extends RemoteWebDriver private final HasExtensions extensions; private final HasFullPageScreenshot fullPageScreenshot; private final HasContext context; - private final Optional biDiUri; - private final Optional biDi; /** * Creates a new FirefoxDriver using the {@link GeckoDriverService#createDefaultService)} server @@ -140,25 +132,7 @@ private FirefoxDriver( new AddHasFullPageScreenshot().getImplementation(getCapabilities(), getExecuteMethod()); context = new AddHasContext().getImplementation(getCapabilities(), getExecuteMethod()); - Capabilities capabilities = super.getCapabilities(); - - Optional webSocketUrl = - Optional.ofNullable((String) capabilities.getCapability("webSocketUrl")); - - this.biDiUri = - webSocketUrl.map( - uri -> { - try { - return new URI(uri); - } catch (URISyntaxException e) { - LOG.warning(e.getMessage()); - } - return null; - }); - - this.biDi = createBiDi(clientConfig, biDiUri); - - this.capabilities = new ImmutableCapabilities(capabilities); + this.capabilities = new ImmutableCapabilities(super.getCapabilities()); } @Beta @@ -242,37 +216,6 @@ public void setContext(FirefoxCommandContext commandContext) { context.setContext(commandContext); } - private Optional createBiDi(ClientConfig clientConfig, Optional biDiUri) { - return biDiUri.map( - (URI wsUri) -> { - HttpClient.Factory clientFactory = HttpClient.Factory.createDefault(); - ClientConfig wsConfig = clientConfig.baseUri(wsUri); - HttpClient wsClient = clientFactory.createClient(wsConfig); - - org.openqa.selenium.bidi.Connection biDiConnection = - new org.openqa.selenium.bidi.Connection(wsClient, wsUri.toString()); - - return new BiDi(biDiConnection, wsConfig.wsTimeout()); - }); - } - - @Override - public Optional maybeGetBiDi() { - return biDi; - } - - @Override - public BiDi getBiDi() { - if (biDiUri.isEmpty()) { - throw new BiDiException( - "Check if this browser version supports BiDi and if the 'webSocketUrl: true' capability" - + " is set."); - } - - return maybeGetBiDi() - .orElseThrow(() -> new BiDiException("Unable to initialize Bidi connection")); - } - @Override public void quit() { super.quit(); diff --git a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java index bce20adce45ee..8040c3259fc77 100644 --- a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java +++ b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java @@ -29,6 +29,8 @@ import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; @@ -42,6 +44,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; import java.util.logging.Level; @@ -77,6 +80,8 @@ import org.openqa.selenium.WebElement; import org.openqa.selenium.WindowType; import org.openqa.selenium.bidi.BiDi; +import org.openqa.selenium.bidi.BiDiException; +import org.openqa.selenium.bidi.Connection; import org.openqa.selenium.bidi.HasBiDi; import org.openqa.selenium.devtools.DevTools; import org.openqa.selenium.devtools.HasDevTools; @@ -106,6 +111,7 @@ @Augmentable public class RemoteWebDriver implements WebDriver, + HasBiDi, JavascriptExecutor, HasCapabilities, HasDownloads, @@ -142,6 +148,8 @@ public class RemoteWebDriver @Nullable private Network remoteNetwork; + private Optional biDi = Optional.empty(); + // For cglib @SuppressWarnings("DataFlowIssue") protected RemoteWebDriver() { @@ -283,6 +291,9 @@ protected void startSession(Capabilities capabilities) { this.capabilities = returnedCapabilities; sessionId = new SessionId(response.getSessionId()); + if (Boolean.TRUE.equals(capabilities.getCapability("webSocketUrl"))) { + this.biDi = createBiDi(); + } } catch (Exception e) { // If session creation fails, stop the driver service to prevent zombie processes if (executor instanceof DriverCommandExecutor) { @@ -422,6 +433,36 @@ public String getPageSource() { return (String) execute(DriverCommand.GET_PAGE_SOURCE).getValue(); } + private Optional createBiDi() { + Object rawUrl = this.capabilities.getCapability("webSocketUrl"); + if (!(rawUrl instanceof String) + || (!((String) rawUrl).startsWith("ws://") && !((String) rawUrl).startsWith("wss://"))) { + throw new BiDiException( + "Check if this browser version supports BiDi and if the" + + " 'webSocketUrl: true' capability is set."); + } + String webSocketUrl = (String) rawUrl; + try { + URI wsUri = new URI(webSocketUrl); + HttpClient.Factory clientFactory = HttpClient.Factory.createDefault(); + ClientConfig wsConfig = this.clientConfig.baseUri(wsUri); + HttpClient wsClient = clientFactory.createClient(wsConfig); + Connection biDiConnection = new Connection(wsClient, wsUri.toString()); + return Optional.of(new BiDi(biDiConnection, wsConfig.wsTimeout())); + } catch (URISyntaxException e) { + throw new BiDiException( + "Check if this browser version supports BiDi and if the" + + " 'webSocketUrl: true' capability is set.", + e); + } + } + + @Deprecated(since = "4.46", forRemoval = true) + @Override + public Optional maybeGetBiDi() { + return biDi; + } + // Misc @Override @@ -447,11 +488,11 @@ public void close() { Object value = response.getValue(); List windowHandles = (ArrayList) value; - if (windowHandles.isEmpty() && this instanceof HasBiDi) { + if (windowHandles.isEmpty()) { // If no top-level browsing contexts are open after calling close, it indicates that the // WebDriver session is closed. // If the WebDriver session is closed, the BiDi session also needs to be closed. - ((HasBiDi) this).maybeGetBiDi().ifPresent(BiDi::close); + biDi.ifPresent(BiDi::close); } } @@ -467,9 +508,7 @@ public void quit() { ((HasDevTools) this).maybeGetDevTools().ifPresent(DevTools::close); } - if (this instanceof HasBiDi) { - ((HasBiDi) this).maybeGetBiDi().ifPresent(BiDi::close); - } + biDi.ifPresent(BiDi::close); execute(DriverCommand.QUIT); } finally { diff --git a/java/src/org/openqa/selenium/safari/SafariOptions.java b/java/src/org/openqa/selenium/safari/SafariOptions.java index ab390e6b99c25..196255a050cf6 100644 --- a/java/src/org/openqa/selenium/safari/SafariOptions.java +++ b/java/src/org/openqa/selenium/safari/SafariOptions.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.Set; +import java.util.logging.Logger; import org.jspecify.annotations.Nullable; import org.openqa.selenium.Capabilities; import org.openqa.selenium.WebDriverException; @@ -49,6 +50,8 @@ */ public class SafariOptions extends AbstractDriverOptions { + private static final Logger LOG = Logger.getLogger(SafariOptions.class.getName()); + public SafariOptions() { setUseTechnologyPreview(false); setCapability(BROWSER_NAME, SAFARI.browserName()); @@ -89,6 +92,19 @@ public SafariOptions merge(Capabilities extraCapabilities) { return newInstance; } + /** + * Enables the WebDriver BiDi protocol. BiDi support in Safari is experimental and requires Safari + * Technology Preview; see {@link #setUseTechnologyPreview(boolean)}. + * + * @return this {@link SafariOptions} instance for chaining + */ + public SafariOptions enableBiDi() { + LOG.warning("Safari's WebDriver BiDi support is experimental and may not work as expected."); + setCapability("webSocketUrl", true); + setCapability(Option.EXPERIMENTAL_WEB_SOCKET_URL, true); + return this; + } + public boolean getAutomaticInspection() { return Boolean.TRUE.equals(getCapability(Option.AUTOMATIC_INSPECTION)); } @@ -156,5 +172,6 @@ private interface Option { // Defined by Apple String AUTOMATIC_INSPECTION = "safari:automaticInspection"; String AUTOMATIC_PROFILING = "safari:automaticProfiling"; + String EXPERIMENTAL_WEB_SOCKET_URL = "safari:experimentalWebSocketUrl"; } } From fd3fcbe63e7b85f0b0a64905ba91e0fcfd786514 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Tue, 7 Jul 2026 11:19:35 +0530 Subject: [PATCH 2/8] [java] Move BiDi creation to RemoteWebDriver. Add enable BiDi to SafariOptions. --- .../selenium/chromium/ChromiumDriver.java | 47 --------------- .../selenium/firefox/FirefoxDriver.java | 59 +------------------ .../selenium/remote/RemoteWebDriver.java | 49 +++++++++++++-- .../openqa/selenium/safari/SafariOptions.java | 17 ++++++ 4 files changed, 62 insertions(+), 110 deletions(-) diff --git a/java/src/org/openqa/selenium/chromium/ChromiumDriver.java b/java/src/org/openqa/selenium/chromium/ChromiumDriver.java index 46ae2d48cb21c..178143445c58e 100644 --- a/java/src/org/openqa/selenium/chromium/ChromiumDriver.java +++ b/java/src/org/openqa/selenium/chromium/ChromiumDriver.java @@ -23,7 +23,6 @@ import static org.openqa.selenium.remote.Browser.OPERA; import java.net.URI; -import java.net.URISyntaxException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -44,8 +43,6 @@ import org.openqa.selenium.ScriptKey; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.bidi.BiDi; -import org.openqa.selenium.bidi.BiDiException; import org.openqa.selenium.bidi.HasBiDi; import org.openqa.selenium.devtools.CdpEndpointFinder; import org.openqa.selenium.devtools.CdpInfo; @@ -87,7 +84,6 @@ public class ChromiumDriver extends RemoteWebDriver private final HasPermissions permissions; private final HasLaunchApp launch; private final Optional devTools; - private final Optional biDi; /** * May be null when the driver does not support casting; initialized during setup if available. @@ -118,22 +114,6 @@ protected ChromiumDriver( HttpClient.Factory factory = HttpClient.Factory.createDefault(); Capabilities originalCapabilities = super.getCapabilities(); - Optional webSocketUrl = - Optional.ofNullable((String) originalCapabilities.getCapability("webSocketUrl")); - - Optional biDiUri = - webSocketUrl.map( - uri -> { - try { - return new URI(uri); - } catch (URISyntaxException e) { - LOG.warning(e.getMessage()); - } - return null; - }); - - this.biDi = createBiDi(biDiUri, clientConfig); - Optional reportedUri = CdpEndpointFinder.getReportedUri(capabilityKey, originalCapabilities); Optional client = @@ -296,33 +276,6 @@ public Optional maybeGetDevTools() { return devTools; } - private Optional createBiDi(Optional biDiUri, ClientConfig clientConfig) { - if (biDiUri.isEmpty()) { - return Optional.empty(); - } - - URI wsUri = - biDiUri.orElseThrow( - () -> - new BiDiException( - "Check if this browser version supports BiDi and if the 'webSocketUrl: true'" - + " capability is set.")); - - HttpClient.Factory clientFactory = HttpClient.Factory.createDefault(); - ClientConfig wsConfig = clientConfig.baseUri(wsUri); - HttpClient wsClient = clientFactory.createClient(wsConfig); - - org.openqa.selenium.bidi.Connection biDiConnection = - new org.openqa.selenium.bidi.Connection(wsClient, wsUri.toString()); - - return Optional.of(new BiDi(biDiConnection, wsConfig.wsTimeout())); - } - - @Override - public Optional maybeGetBiDi() { - return biDi; - } - @Override public List> getCastSinks() { if (this.casting == null) { diff --git a/java/src/org/openqa/selenium/firefox/FirefoxDriver.java b/java/src/org/openqa/selenium/firefox/FirefoxDriver.java index 2546a8556e971..e4166404a0347 100644 --- a/java/src/org/openqa/selenium/firefox/FirefoxDriver.java +++ b/java/src/org/openqa/selenium/firefox/FirefoxDriver.java @@ -19,11 +19,8 @@ import static org.openqa.selenium.remote.CapabilityType.PROXY; -import java.net.URI; -import java.net.URISyntaxException; import java.nio.file.Path; import java.util.Map; -import java.util.Optional; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -34,8 +31,6 @@ import org.openqa.selenium.OutputType; import org.openqa.selenium.Proxy; import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.bidi.BiDi; -import org.openqa.selenium.bidi.BiDiException; import org.openqa.selenium.bidi.HasBiDi; import org.openqa.selenium.internal.Require; import org.openqa.selenium.remote.CommandInfo; @@ -43,7 +38,6 @@ import org.openqa.selenium.remote.RemoteWebDriver; import org.openqa.selenium.remote.RemoteWebDriverBuilder; import org.openqa.selenium.remote.http.ClientConfig; -import org.openqa.selenium.remote.http.HttpClient; import org.openqa.selenium.remote.service.DriverCommandExecutor; import org.openqa.selenium.remote.service.DriverFinder; import org.openqa.selenium.remote.service.DriverService; @@ -71,8 +65,6 @@ public class FirefoxDriver extends RemoteWebDriver private final HasExtensions extensions; private final HasFullPageScreenshot fullPageScreenshot; private final HasContext context; - private final Optional biDiUri; - private final Optional biDi; /** * Creates a new FirefoxDriver using the {@link GeckoDriverService#createDefaultService)} server @@ -140,25 +132,7 @@ private FirefoxDriver( new AddHasFullPageScreenshot().getImplementation(getCapabilities(), getExecuteMethod()); context = new AddHasContext().getImplementation(getCapabilities(), getExecuteMethod()); - Capabilities capabilities = super.getCapabilities(); - - Optional webSocketUrl = - Optional.ofNullable((String) capabilities.getCapability("webSocketUrl")); - - this.biDiUri = - webSocketUrl.map( - uri -> { - try { - return new URI(uri); - } catch (URISyntaxException e) { - LOG.warning(e.getMessage()); - } - return null; - }); - - this.biDi = createBiDi(clientConfig, biDiUri); - - this.capabilities = new ImmutableCapabilities(capabilities); + this.capabilities = new ImmutableCapabilities(super.getCapabilities()); } @Beta @@ -242,37 +216,6 @@ public void setContext(FirefoxCommandContext commandContext) { context.setContext(commandContext); } - private Optional createBiDi(ClientConfig clientConfig, Optional biDiUri) { - return biDiUri.map( - (URI wsUri) -> { - HttpClient.Factory clientFactory = HttpClient.Factory.createDefault(); - ClientConfig wsConfig = clientConfig.baseUri(wsUri); - HttpClient wsClient = clientFactory.createClient(wsConfig); - - org.openqa.selenium.bidi.Connection biDiConnection = - new org.openqa.selenium.bidi.Connection(wsClient, wsUri.toString()); - - return new BiDi(biDiConnection, wsConfig.wsTimeout()); - }); - } - - @Override - public Optional maybeGetBiDi() { - return biDi; - } - - @Override - public BiDi getBiDi() { - if (biDiUri.isEmpty()) { - throw new BiDiException( - "Check if this browser version supports BiDi and if the 'webSocketUrl: true' capability" - + " is set."); - } - - return maybeGetBiDi() - .orElseThrow(() -> new BiDiException("Unable to initialize Bidi connection")); - } - @Override public void quit() { super.quit(); diff --git a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java index bce20adce45ee..8040c3259fc77 100644 --- a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java +++ b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java @@ -29,6 +29,8 @@ import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; @@ -42,6 +44,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; import java.util.logging.Level; @@ -77,6 +80,8 @@ import org.openqa.selenium.WebElement; import org.openqa.selenium.WindowType; import org.openqa.selenium.bidi.BiDi; +import org.openqa.selenium.bidi.BiDiException; +import org.openqa.selenium.bidi.Connection; import org.openqa.selenium.bidi.HasBiDi; import org.openqa.selenium.devtools.DevTools; import org.openqa.selenium.devtools.HasDevTools; @@ -106,6 +111,7 @@ @Augmentable public class RemoteWebDriver implements WebDriver, + HasBiDi, JavascriptExecutor, HasCapabilities, HasDownloads, @@ -142,6 +148,8 @@ public class RemoteWebDriver @Nullable private Network remoteNetwork; + private Optional biDi = Optional.empty(); + // For cglib @SuppressWarnings("DataFlowIssue") protected RemoteWebDriver() { @@ -283,6 +291,9 @@ protected void startSession(Capabilities capabilities) { this.capabilities = returnedCapabilities; sessionId = new SessionId(response.getSessionId()); + if (Boolean.TRUE.equals(capabilities.getCapability("webSocketUrl"))) { + this.biDi = createBiDi(); + } } catch (Exception e) { // If session creation fails, stop the driver service to prevent zombie processes if (executor instanceof DriverCommandExecutor) { @@ -422,6 +433,36 @@ public String getPageSource() { return (String) execute(DriverCommand.GET_PAGE_SOURCE).getValue(); } + private Optional createBiDi() { + Object rawUrl = this.capabilities.getCapability("webSocketUrl"); + if (!(rawUrl instanceof String) + || (!((String) rawUrl).startsWith("ws://") && !((String) rawUrl).startsWith("wss://"))) { + throw new BiDiException( + "Check if this browser version supports BiDi and if the" + + " 'webSocketUrl: true' capability is set."); + } + String webSocketUrl = (String) rawUrl; + try { + URI wsUri = new URI(webSocketUrl); + HttpClient.Factory clientFactory = HttpClient.Factory.createDefault(); + ClientConfig wsConfig = this.clientConfig.baseUri(wsUri); + HttpClient wsClient = clientFactory.createClient(wsConfig); + Connection biDiConnection = new Connection(wsClient, wsUri.toString()); + return Optional.of(new BiDi(biDiConnection, wsConfig.wsTimeout())); + } catch (URISyntaxException e) { + throw new BiDiException( + "Check if this browser version supports BiDi and if the" + + " 'webSocketUrl: true' capability is set.", + e); + } + } + + @Deprecated(since = "4.46", forRemoval = true) + @Override + public Optional maybeGetBiDi() { + return biDi; + } + // Misc @Override @@ -447,11 +488,11 @@ public void close() { Object value = response.getValue(); List windowHandles = (ArrayList) value; - if (windowHandles.isEmpty() && this instanceof HasBiDi) { + if (windowHandles.isEmpty()) { // If no top-level browsing contexts are open after calling close, it indicates that the // WebDriver session is closed. // If the WebDriver session is closed, the BiDi session also needs to be closed. - ((HasBiDi) this).maybeGetBiDi().ifPresent(BiDi::close); + biDi.ifPresent(BiDi::close); } } @@ -467,9 +508,7 @@ public void quit() { ((HasDevTools) this).maybeGetDevTools().ifPresent(DevTools::close); } - if (this instanceof HasBiDi) { - ((HasBiDi) this).maybeGetBiDi().ifPresent(BiDi::close); - } + biDi.ifPresent(BiDi::close); execute(DriverCommand.QUIT); } finally { diff --git a/java/src/org/openqa/selenium/safari/SafariOptions.java b/java/src/org/openqa/selenium/safari/SafariOptions.java index ab390e6b99c25..196255a050cf6 100644 --- a/java/src/org/openqa/selenium/safari/SafariOptions.java +++ b/java/src/org/openqa/selenium/safari/SafariOptions.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.Set; +import java.util.logging.Logger; import org.jspecify.annotations.Nullable; import org.openqa.selenium.Capabilities; import org.openqa.selenium.WebDriverException; @@ -49,6 +50,8 @@ */ public class SafariOptions extends AbstractDriverOptions { + private static final Logger LOG = Logger.getLogger(SafariOptions.class.getName()); + public SafariOptions() { setUseTechnologyPreview(false); setCapability(BROWSER_NAME, SAFARI.browserName()); @@ -89,6 +92,19 @@ public SafariOptions merge(Capabilities extraCapabilities) { return newInstance; } + /** + * Enables the WebDriver BiDi protocol. BiDi support in Safari is experimental and requires Safari + * Technology Preview; see {@link #setUseTechnologyPreview(boolean)}. + * + * @return this {@link SafariOptions} instance for chaining + */ + public SafariOptions enableBiDi() { + LOG.warning("Safari's WebDriver BiDi support is experimental and may not work as expected."); + setCapability("webSocketUrl", true); + setCapability(Option.EXPERIMENTAL_WEB_SOCKET_URL, true); + return this; + } + public boolean getAutomaticInspection() { return Boolean.TRUE.equals(getCapability(Option.AUTOMATIC_INSPECTION)); } @@ -156,5 +172,6 @@ private interface Option { // Defined by Apple String AUTOMATIC_INSPECTION = "safari:automaticInspection"; String AUTOMATIC_PROFILING = "safari:automaticProfiling"; + String EXPERIMENTAL_WEB_SOCKET_URL = "safari:experimentalWebSocketUrl"; } } From 56271824706dffe8c421768f0dc36c23e118c51d Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Tue, 7 Jul 2026 11:43:26 +0530 Subject: [PATCH 3/8] Address code comments --- .../openqa/selenium/remote/RemoteWebDriver.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java index 8040c3259fc77..468b25abbea0d 100644 --- a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java +++ b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java @@ -80,7 +80,6 @@ import org.openqa.selenium.WebElement; import org.openqa.selenium.WindowType; import org.openqa.selenium.bidi.BiDi; -import org.openqa.selenium.bidi.BiDiException; import org.openqa.selenium.bidi.Connection; import org.openqa.selenium.bidi.HasBiDi; import org.openqa.selenium.devtools.DevTools; @@ -437,9 +436,8 @@ private Optional createBiDi() { Object rawUrl = this.capabilities.getCapability("webSocketUrl"); if (!(rawUrl instanceof String) || (!((String) rawUrl).startsWith("ws://") && !((String) rawUrl).startsWith("wss://"))) { - throw new BiDiException( - "Check if this browser version supports BiDi and if the" - + " 'webSocketUrl: true' capability is set."); + LOG.warning("BiDi was requested but the remote end did not return a valid webSocketUrl."); + return Optional.empty(); } String webSocketUrl = (String) rawUrl; try { @@ -450,10 +448,10 @@ private Optional createBiDi() { Connection biDiConnection = new Connection(wsClient, wsUri.toString()); return Optional.of(new BiDi(biDiConnection, wsConfig.wsTimeout())); } catch (URISyntaxException e) { - throw new BiDiException( - "Check if this browser version supports BiDi and if the" - + " 'webSocketUrl: true' capability is set.", - e); + LOG.warning( + "BiDi was requested but the remote end returned an invalid webSocketUrl: " + + webSocketUrl); + return Optional.empty(); } } From 10347f6753e47433079ed8348e9ec3101cc6fea3 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Tue, 7 Jul 2026 12:20:35 +0530 Subject: [PATCH 4/8] Fix code comments --- .../org/openqa/selenium/remote/RemoteWebDriver.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java index 468b25abbea0d..0e642d258d45c 100644 --- a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java +++ b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java @@ -436,7 +436,9 @@ private Optional createBiDi() { Object rawUrl = this.capabilities.getCapability("webSocketUrl"); if (!(rawUrl instanceof String) || (!((String) rawUrl).startsWith("ws://") && !((String) rawUrl).startsWith("wss://"))) { - LOG.warning("BiDi was requested but the remote end did not return a valid webSocketUrl."); + LOG.warning( + "BiDi was requested but the remote end did not return a valid webSocketUrl." + + " BiDi connection will not be established."); return Optional.empty(); } String webSocketUrl = (String) rawUrl; @@ -448,9 +450,11 @@ private Optional createBiDi() { Connection biDiConnection = new Connection(wsClient, wsUri.toString()); return Optional.of(new BiDi(biDiConnection, wsConfig.wsTimeout())); } catch (URISyntaxException e) { - LOG.warning( - "BiDi was requested but the remote end returned an invalid webSocketUrl: " - + webSocketUrl); + LOG.log( + Level.WARNING, + "BiDi was requested but the remote end returned an invalid webSocketUrl." + + " BiDi connection will not be established.", + e); return Optional.empty(); } } From 26bb8e605a6bf87730306255cc71d2429a69bd8f Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Tue, 7 Jul 2026 12:44:32 +0530 Subject: [PATCH 5/8] Remove a test that relied on BiDiProvider --- .../selenium/grid/router/DistributedTest.java | 43 ------------------- 1 file changed, 43 deletions(-) diff --git a/java/test/org/openqa/selenium/grid/router/DistributedTest.java b/java/test/org/openqa/selenium/grid/router/DistributedTest.java index 8912cd671967a..5e76c84cf4bb8 100644 --- a/java/test/org/openqa/selenium/grid/router/DistributedTest.java +++ b/java/test/org/openqa/selenium/grid/router/DistributedTest.java @@ -194,47 +194,4 @@ void clientTimeoutDoesNotLeakARunningBrowser() throws Exception { Safely.safelyCall(healthy::quit); } } - - @Test - void connectionLimitIsRespected() { - assertThat(server.isStarted()).isTrue(); - - // don't use the RemoteWebDriver.builder here, using it does create an unknown number of - // connections - RemoteWebDriver driver = new RemoteWebDriver(server.getUrl(), browser.getCapabilities()); - - try { - Capabilities caps = driver.getCapabilities(); - BiDiProvider biDiProvider = new BiDiProvider(); - - ExecuteMethod executeMethod = new RemoteExecuteMethod(driver); - BiDi cnn1 = biDiProvider.getImplementation(caps, executeMethod).getBiDi(); - BiDi cnn2 = biDiProvider.getImplementation(caps, executeMethod).getBiDi(); - BiDi cnn3 = biDiProvider.getImplementation(caps, executeMethod).getBiDi(); - - assertThatThrownBy(() -> biDiProvider.getImplementation(caps, executeMethod).getBiDi()) - .isInstanceOf(ConnectionFailedException.class) - .hasMessageStartingWith("JdkWebSocket initial request execution error"); - cnn1.close(); - BiDi cnn4 = biDiProvider.getImplementation(caps, executeMethod).getBiDi(); - - assertThatThrownBy(() -> biDiProvider.getImplementation(caps, executeMethod).getBiDi()) - .isInstanceOf(ConnectionFailedException.class) - .hasMessageStartingWith("JdkWebSocket initial request execution error"); - cnn2.close(); - cnn3.close(); - BiDi cnn5 = biDiProvider.getImplementation(caps, executeMethod).getBiDi(); - BiDi cnn6 = biDiProvider.getImplementation(caps, executeMethod).getBiDi(); - - assertThatThrownBy(() -> biDiProvider.getImplementation(caps, executeMethod).getBiDi()) - .isInstanceOf(ConnectionFailedException.class) - .hasMessageStartingWith("JdkWebSocket initial request execution error"); - - cnn4.close(); - cnn5.close(); - cnn6.close(); - } finally { - Safely.safelyCall(driver::quit); - } - } } From ebae01d4b691face5bed10f2639916a3bf02d475 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Tue, 7 Jul 2026 12:57:38 +0530 Subject: [PATCH 6/8] Fix formatting --- .../org/openqa/selenium/grid/router/DistributedTest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/java/test/org/openqa/selenium/grid/router/DistributedTest.java b/java/test/org/openqa/selenium/grid/router/DistributedTest.java index 5e76c84cf4bb8..5572e91d8f553 100644 --- a/java/test/org/openqa/selenium/grid/router/DistributedTest.java +++ b/java/test/org/openqa/selenium/grid/router/DistributedTest.java @@ -29,11 +29,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.openqa.selenium.Capabilities; import org.openqa.selenium.SessionNotCreatedException; import org.openqa.selenium.WebDriver; -import org.openqa.selenium.bidi.BiDi; -import org.openqa.selenium.bidi.BiDiProvider; import org.openqa.selenium.grid.config.MapConfig; import org.openqa.selenium.grid.config.MemoizedConfig; import org.openqa.selenium.grid.config.TomlConfig; @@ -43,11 +40,8 @@ import org.openqa.selenium.json.Json; import org.openqa.selenium.json.JsonInput; import org.openqa.selenium.netty.server.NettyServer; -import org.openqa.selenium.remote.ExecuteMethod; -import org.openqa.selenium.remote.RemoteExecuteMethod; import org.openqa.selenium.remote.RemoteWebDriver; import org.openqa.selenium.remote.http.ClientConfig; -import org.openqa.selenium.remote.http.ConnectionFailedException; import org.openqa.selenium.remote.http.Contents; import org.openqa.selenium.remote.http.HttpClient; import org.openqa.selenium.remote.http.HttpMethod; From 0a205475ab51fb4015ece486a624de6a602469a2 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Tue, 7 Jul 2026 13:06:56 +0530 Subject: [PATCH 7/8] Avoid httpclient leaks on failed connection --- .../selenium/remote/RemoteWebDriver.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java index 0e642d258d45c..fd96beeffdfeb 100644 --- a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java +++ b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java @@ -436,9 +436,7 @@ private Optional createBiDi() { Object rawUrl = this.capabilities.getCapability("webSocketUrl"); if (!(rawUrl instanceof String) || (!((String) rawUrl).startsWith("ws://") && !((String) rawUrl).startsWith("wss://"))) { - LOG.warning( - "BiDi was requested but the remote end did not return a valid webSocketUrl." - + " BiDi connection will not be established."); + LOG.warning("BiDi was requested but the remote end did not return a valid webSocketUrl."); return Optional.empty(); } String webSocketUrl = (String) rawUrl; @@ -447,13 +445,21 @@ private Optional createBiDi() { HttpClient.Factory clientFactory = HttpClient.Factory.createDefault(); ClientConfig wsConfig = this.clientConfig.baseUri(wsUri); HttpClient wsClient = clientFactory.createClient(wsConfig); - Connection biDiConnection = new Connection(wsClient, wsUri.toString()); - return Optional.of(new BiDi(biDiConnection, wsConfig.wsTimeout())); + try { + Connection biDiConnection = new Connection(wsClient, wsUri.toString()); + return Optional.of(new BiDi(biDiConnection, wsConfig.wsTimeout())); + } catch (RuntimeException e) { + wsClient.close(); + LOG.log( + Level.WARNING, + "BiDi was requested but the WebSocket connection could not be established.", + e); + return Optional.empty(); + } } catch (URISyntaxException e) { LOG.log( Level.WARNING, - "BiDi was requested but the remote end returned an invalid webSocketUrl." - + " BiDi connection will not be established.", + "BiDi was requested but the remote end returned an invalid webSocketUrl.", e); return Optional.empty(); } From 69d616129ad8e18cdc0353e0eb79f9ccfb5b2ba7 Mon Sep 17 00:00:00 2001 From: Puja Jagani Date: Tue, 7 Jul 2026 13:51:01 +0530 Subject: [PATCH 8/8] Address comments --- .../selenium/remote/RemoteWebDriver.java | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java index fd96beeffdfeb..c2e2dbe0423ff 100644 --- a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java +++ b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java @@ -434,28 +434,13 @@ public String getPageSource() { private Optional createBiDi() { Object rawUrl = this.capabilities.getCapability("webSocketUrl"); - if (!(rawUrl instanceof String) - || (!((String) rawUrl).startsWith("ws://") && !((String) rawUrl).startsWith("wss://"))) { - LOG.warning("BiDi was requested but the remote end did not return a valid webSocketUrl."); + if (!(rawUrl instanceof String)) { return Optional.empty(); } - String webSocketUrl = (String) rawUrl; + String webSocketUrl = ((String) rawUrl).trim(); + URI wsUri; try { - URI wsUri = new URI(webSocketUrl); - HttpClient.Factory clientFactory = HttpClient.Factory.createDefault(); - ClientConfig wsConfig = this.clientConfig.baseUri(wsUri); - HttpClient wsClient = clientFactory.createClient(wsConfig); - try { - Connection biDiConnection = new Connection(wsClient, wsUri.toString()); - return Optional.of(new BiDi(biDiConnection, wsConfig.wsTimeout())); - } catch (RuntimeException e) { - wsClient.close(); - LOG.log( - Level.WARNING, - "BiDi was requested but the WebSocket connection could not be established.", - e); - return Optional.empty(); - } + wsUri = new URI(webSocketUrl); } catch (URISyntaxException e) { LOG.log( Level.WARNING, @@ -463,6 +448,25 @@ private Optional createBiDi() { e); return Optional.empty(); } + String scheme = wsUri.getScheme(); + if (scheme == null || (!scheme.equalsIgnoreCase("ws") && !scheme.equalsIgnoreCase("wss"))) { + LOG.warning("BiDi was requested but the remote end did not return a valid webSocketUrl."); + return Optional.empty(); + } + HttpClient.Factory clientFactory = HttpClient.Factory.createDefault(); + ClientConfig wsConfig = this.clientConfig.baseUri(wsUri); + HttpClient wsClient = clientFactory.createClient(wsConfig); + try { + Connection biDiConnection = new Connection(wsClient, wsUri.toString()); + return Optional.of(new BiDi(biDiConnection, wsConfig.wsTimeout())); + } catch (RuntimeException e) { + wsClient.close(); + LOG.log( + Level.WARNING, + "BiDi was requested but the WebSocket connection could not be established.", + e); + return Optional.empty(); + } } @Deprecated(since = "4.46", forRemoval = true)