From 78ce161e059455ffaeba3fcd319ed194939412a7 Mon Sep 17 00:00:00 2001 From: Krishna Date: Mon, 16 Mar 2026 22:54:35 -0400 Subject: [PATCH 1/5] [java][bidi] Route navigation commands through BiDi Implements get(), back(), forward(), and refresh() via BrowsingContext when webSocketUrl capability is present, falling back to Classic otherwise. Maps pageLoadStrategy to ReadinessState: normal->COMPLETE, eager->INTERACTIVE, none->NONE. Adds integration tests covering all navigation commands with BiDi enabled. Fixes #13995 --- .../org/openqa/selenium/remote/BUILD.bazel | 1 + .../selenium/remote/RemoteWebDriver.java | 38 +++++- .../browsingcontext/BiDiNavigationTest.java | 113 ++++++++++++++++++ 3 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 java/test/org/openqa/selenium/bidi/browsingcontext/BiDiNavigationTest.java diff --git a/java/src/org/openqa/selenium/remote/BUILD.bazel b/java/src/org/openqa/selenium/remote/BUILD.bazel index 71bfde2e5d0b5..4fc796ddcb3d7 100644 --- a/java/src/org/openqa/selenium/remote/BUILD.bazel +++ b/java/src/org/openqa/selenium/remote/BUILD.bazel @@ -63,6 +63,7 @@ java_library( deps = [ "//java/src/org/openqa/selenium:core", "//java/src/org/openqa/selenium/bidi", + "//java/src/org/openqa/selenium/bidi/browsingcontext", "//java/src/org/openqa/selenium/bidi/log", "//java/src/org/openqa/selenium/bidi/module", "//java/src/org/openqa/selenium/bidi/network", diff --git a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java index 0ed6e13909865..beb11967e782b 100644 --- a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java +++ b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java @@ -77,6 +77,8 @@ import org.openqa.selenium.WindowType; import org.openqa.selenium.bidi.BiDi; import org.openqa.selenium.bidi.HasBiDi; +import org.openqa.selenium.bidi.browsingcontext.BrowsingContext; +import org.openqa.selenium.bidi.browsingcontext.ReadinessState; import org.openqa.selenium.devtools.DevTools; import org.openqa.selenium.devtools.HasDevTools; import org.openqa.selenium.federatedcredentialmanagement.FederatedCredentialManagementDialog; @@ -370,7 +372,22 @@ public Capabilities getCapabilities() { @Override public void get(String url) { - execute(DriverCommand.GET(url)); + if (getCapabilities().getCapability("webSocketUrl") != null) { + new BrowsingContext(this, getWindowHandle()) + .navigate(url, getReadinessState()); + } else { + execute(DriverCommand.GET(url)); + } + } + + private ReadinessState getReadinessState() { + Object strategy = getCapabilities().getCapability(CapabilityType.PAGE_LOAD_STRATEGY); + if ("eager".equals(strategy)) { + return ReadinessState.INTERACTIVE; + } else if ("none".equals(strategy)) { + return ReadinessState.NONE; + } + return ReadinessState.COMPLETE; } @Override @@ -1214,12 +1231,20 @@ private class RemoteNavigation implements Navigation { @Override public void back() { - execute(DriverCommand.GO_BACK); + if (getCapabilities().getCapability("webSocketUrl") != null) { + new BrowsingContext(RemoteWebDriver.this, getWindowHandle()).back(); + } else { + execute(DriverCommand.GO_BACK); + } } @Override public void forward() { - execute(DriverCommand.GO_FORWARD); + if (getCapabilities().getCapability("webSocketUrl") != null) { + new BrowsingContext(RemoteWebDriver.this, getWindowHandle()).forward(); + } else { + execute(DriverCommand.GO_FORWARD); + } } @Override @@ -1234,7 +1259,12 @@ public void to(URL url) { @Override public void refresh() { - execute(DriverCommand.REFRESH); + if (getCapabilities().getCapability("webSocketUrl") != null) { + new BrowsingContext(RemoteWebDriver.this, getWindowHandle()) + .reload(getReadinessState()); + } else { + execute(DriverCommand.REFRESH); + } } } diff --git a/java/test/org/openqa/selenium/bidi/browsingcontext/BiDiNavigationTest.java b/java/test/org/openqa/selenium/bidi/browsingcontext/BiDiNavigationTest.java new file mode 100644 index 0000000000000..fc1febe132db1 --- /dev/null +++ b/java/test/org/openqa/selenium/bidi/browsingcontext/BiDiNavigationTest.java @@ -0,0 +1,113 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.openqa.selenium.bidi.browsingcontext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openqa.selenium.support.ui.ExpectedConditions.titleIs; +import static org.openqa.selenium.support.ui.ExpectedConditions.visibilityOfElementLocated; +import static org.openqa.selenium.testing.drivers.Browser.EDGE; + +import java.net.MalformedURLException; +import java.net.URL; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.testing.JupiterTestBase; +import org.openqa.selenium.testing.NeedsFreshDriver; +import org.openqa.selenium.testing.NotYetImplemented; + +class BiDiNavigationTest extends JupiterTestBase { + + @Test + @NeedsFreshDriver + void driverGetNavigatesToUrlViaBiDi() { + String url = appServer.whereIs("formPage.html"); + driver.get(url); + assertThat(driver.getCurrentUrl()).contains("formPage.html"); + assertThat(driver.getTitle()).isEqualTo("We Leave From Here"); + } + + @Test + @NeedsFreshDriver + void driverGetNavigatesToSecondUrlViaBiDi() { + driver.get(pages.formPage); + String url = appServer.whereIs("simpleTest.html"); + driver.get(url); + assertThat(driver.getCurrentUrl()).contains("simpleTest.html"); + } + + @Test + @NeedsFreshDriver + @NotYetImplemented(EDGE) + void navigateToStringUrlViaNavigationTo() { + String url = appServer.whereIs("formPage.html"); + driver.navigate().to(url); + assertThat(driver.getCurrentUrl()).contains("formPage.html"); + assertThat(driver.getTitle()).isEqualTo("We Leave From Here"); + } + + @Test + @NeedsFreshDriver + @NotYetImplemented(EDGE) + void navigateToUrlObjectViaNavigationTo() throws MalformedURLException { + URL url = new URL(appServer.whereIs("formPage.html")); + driver.navigate().to(url); + assertThat(driver.getCurrentUrl()).contains("formPage.html"); + assertThat(driver.getTitle()).isEqualTo("We Leave From Here"); + } + + @Test + @NeedsFreshDriver + @NotYetImplemented(EDGE) + void navigateBackTraversesHistory() { + driver.get(pages.formPage); + wait.until(visibilityOfElementLocated(By.id("imageButton"))).submit(); + wait.until(titleIs("We Arrive Here")); + + driver.navigate().back(); + wait.until(titleIs("We Leave From Here")); + } + + @Test + @NeedsFreshDriver + @NotYetImplemented(EDGE) + void navigateForwardTraversesHistory() { + driver.get(pages.formPage); + wait.until(visibilityOfElementLocated(By.id("imageButton"))).submit(); + wait.until(titleIs("We Arrive Here")); + + driver.navigate().back(); + wait.until(titleIs("We Leave From Here")); + + driver.navigate().forward(); + wait.until(titleIs("We Arrive Here")); + } + + @Test + @NeedsFreshDriver + @NotYetImplemented(EDGE) + void refreshReloadsCurrentPage() { + String url = appServer.whereIs("formPage.html"); + driver.get(url); + assertThat(driver.getTitle()).isEqualTo("We Leave From Here"); + + driver.navigate().refresh(); + + assertThat(driver.getCurrentUrl()).contains("formPage.html"); + assertThat(driver.getTitle()).isEqualTo("We Leave From Here"); + } +} From 50a7aac3f17b74014df2f3beecc154c204165021 Mon Sep 17 00:00:00 2001 From: Krishna Date: Wed, 18 Mar 2026 08:58:52 -0400 Subject: [PATCH 2/5] [java][bidi] Fix BiDi detection and readiness state mapping - Guard BiDi path with instanceof HasBiDi check to prevent IllegalArgumentException on plain RemoteWebDriver instances - Check webSocketUrl instanceof String (not just != null) to avoid false positive on Boolean.TRUE during session setup - Handle PageLoadStrategy as both enum and String when mapping to ReadinessState to fix eager/none being silently ignored Addresses review feedback on #13995 --- .../selenium/remote/RemoteWebDriver.java | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java index beb11967e782b..4e15431059cb3 100644 --- a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java +++ b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java @@ -61,6 +61,7 @@ import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.MutableCapabilities; import org.openqa.selenium.NoAlertPresentException; +import org.openqa.selenium.PageLoadStrategy; import org.openqa.selenium.NoSuchFrameException; import org.openqa.selenium.NoSuchWindowException; import org.openqa.selenium.OutputType; @@ -372,7 +373,7 @@ public Capabilities getCapabilities() { @Override public void get(String url) { - if (getCapabilities().getCapability("webSocketUrl") != null) { + if (isBiDiEnabled()) { new BrowsingContext(this, getWindowHandle()) .navigate(url, getReadinessState()); } else { @@ -380,11 +381,24 @@ public void get(String url) { } } + // BiDi is active when the driver implements HasBiDi and the session returned a WebSocket URL + // (a String), not just the boolean request capability that was sent at session creation. + private boolean isBiDiEnabled() { + return this instanceof HasBiDi + && getCapabilities().getCapability("webSocketUrl") instanceof String; + } + private ReadinessState getReadinessState() { - Object strategy = getCapabilities().getCapability(CapabilityType.PAGE_LOAD_STRATEGY); - if ("eager".equals(strategy)) { + Object raw = getCapabilities().getCapability(CapabilityType.PAGE_LOAD_STRATEGY); + // The capability may be a PageLoadStrategy enum (set locally) or a String (deserialized from + // JSON), so normalise to the enum via toString() before comparing. + PageLoadStrategy strategy = + raw instanceof PageLoadStrategy + ? (PageLoadStrategy) raw + : PageLoadStrategy.fromString(raw == null ? null : raw.toString()); + if (PageLoadStrategy.EAGER.equals(strategy)) { return ReadinessState.INTERACTIVE; - } else if ("none".equals(strategy)) { + } else if (PageLoadStrategy.NONE.equals(strategy)) { return ReadinessState.NONE; } return ReadinessState.COMPLETE; @@ -1231,7 +1245,7 @@ private class RemoteNavigation implements Navigation { @Override public void back() { - if (getCapabilities().getCapability("webSocketUrl") != null) { + if (isBiDiEnabled()) { new BrowsingContext(RemoteWebDriver.this, getWindowHandle()).back(); } else { execute(DriverCommand.GO_BACK); @@ -1240,7 +1254,7 @@ public void back() { @Override public void forward() { - if (getCapabilities().getCapability("webSocketUrl") != null) { + if (isBiDiEnabled()) { new BrowsingContext(RemoteWebDriver.this, getWindowHandle()).forward(); } else { execute(DriverCommand.GO_FORWARD); @@ -1259,7 +1273,7 @@ public void to(URL url) { @Override public void refresh() { - if (getCapabilities().getCapability("webSocketUrl") != null) { + if (isBiDiEnabled()) { new BrowsingContext(RemoteWebDriver.this, getWindowHandle()) .reload(getReadinessState()); } else { From b33f9a189f78c6d07b41e78ca284cd8e7283d6b9 Mon Sep 17 00:00:00 2001 From: Krishna Date: Wed, 18 Mar 2026 14:58:50 -0400 Subject: [PATCH 3/5] [java][bidi] Fix code formatting --- java/src/org/openqa/selenium/remote/RemoteWebDriver.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java index 4e15431059cb3..f921af004c8d7 100644 --- a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java +++ b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java @@ -61,10 +61,10 @@ import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.MutableCapabilities; import org.openqa.selenium.NoAlertPresentException; -import org.openqa.selenium.PageLoadStrategy; import org.openqa.selenium.NoSuchFrameException; import org.openqa.selenium.NoSuchWindowException; import org.openqa.selenium.OutputType; +import org.openqa.selenium.PageLoadStrategy; import org.openqa.selenium.Pdf; import org.openqa.selenium.Platform; import org.openqa.selenium.Point; @@ -374,8 +374,7 @@ public Capabilities getCapabilities() { @Override public void get(String url) { if (isBiDiEnabled()) { - new BrowsingContext(this, getWindowHandle()) - .navigate(url, getReadinessState()); + new BrowsingContext(this, getWindowHandle()).navigate(url, getReadinessState()); } else { execute(DriverCommand.GET(url)); } @@ -1274,8 +1273,7 @@ public void to(URL url) { @Override public void refresh() { if (isBiDiEnabled()) { - new BrowsingContext(RemoteWebDriver.this, getWindowHandle()) - .reload(getReadinessState()); + new BrowsingContext(RemoteWebDriver.this, getWindowHandle()).reload(getReadinessState()); } else { execute(DriverCommand.REFRESH); } From 4fc04b48dee7d58573e409d838dfe879137be6c4 Mon Sep 17 00:00:00 2001 From: Krishna Date: Sat, 21 Mar 2026 14:23:41 -0400 Subject: [PATCH 4/5] [java][bidi] Handle user prompts during BiDi navigation --- .../selenium/remote/RemoteWebDriver.java | 87 ++++++++++++++++++- 1 file changed, 83 insertions(+), 4 deletions(-) diff --git a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java index f921af004c8d7..87c4f4053b79e 100644 --- a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java +++ b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java @@ -26,6 +26,7 @@ import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.StringReader; import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; @@ -72,14 +73,19 @@ import org.openqa.selenium.SearchContext; import org.openqa.selenium.SessionNotCreatedException; import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.UnexpectedAlertBehaviour; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.WebElement; import org.openqa.selenium.WindowType; import org.openqa.selenium.bidi.BiDi; +import org.openqa.selenium.bidi.Event; import org.openqa.selenium.bidi.HasBiDi; import org.openqa.selenium.bidi.browsingcontext.BrowsingContext; import org.openqa.selenium.bidi.browsingcontext.ReadinessState; +import org.openqa.selenium.bidi.browsingcontext.UserPromptOpened; +import org.openqa.selenium.json.Json; +import org.openqa.selenium.json.JsonInput; import org.openqa.selenium.devtools.DevTools; import org.openqa.selenium.devtools.HasDevTools; import org.openqa.selenium.federatedcredentialmanagement.FederatedCredentialManagementDialog; @@ -374,7 +380,10 @@ public Capabilities getCapabilities() { @Override public void get(String url) { if (isBiDiEnabled()) { - new BrowsingContext(this, getWindowHandle()).navigate(url, getReadinessState()); + String contextId = getWindowHandle(); + navigateViaBiDi( + contextId, + () -> new BrowsingContext(this, contextId).navigate(url, getReadinessState())); } else { execute(DriverCommand.GET(url)); } @@ -403,6 +412,68 @@ private ReadinessState getReadinessState() { return ReadinessState.COMPLETE; } + private static final Json BIDI_JSON = new Json(); + + // Shared event definition for browsingContext.userPromptOpened used during navigation. + private static final Event USER_PROMPT_OPENED_EVENT = + new Event<>( + "browsingContext.userPromptOpened", + params -> { + try (StringReader reader = new StringReader(BIDI_JSON.toJson(params)); + JsonInput input = BIDI_JSON.newInput(reader)) { + return input.readNonNull(UserPromptOpened.class); + } + }); + + private UnexpectedAlertBehaviour getUnhandledPromptBehaviour() { + Object raw = getCapabilities().getCapability(CapabilityType.UNHANDLED_PROMPT_BEHAVIOUR); + if (raw instanceof UnexpectedAlertBehaviour) { + return (UnexpectedAlertBehaviour) raw; + } + if (raw instanceof String) { + return UnexpectedAlertBehaviour.fromString((String) raw); + } + // W3C WebDriver spec default is "dismiss and notify" + return UnexpectedAlertBehaviour.DISMISS_AND_NOTIFY; + } + + // Wraps a BiDi navigation call with a userPromptOpened listener that handles any alerts + // according to the unhandledPromptBehavior capability. This replicates, for BiDi navigation, + // the automatic prompt handling that classic WebDriver delegates to the browser via the + // capability. + private void navigateViaBiDi(String contextId, Runnable navigation) { + UnexpectedAlertBehaviour behaviour = getUnhandledPromptBehaviour(); + if (behaviour == UnexpectedAlertBehaviour.IGNORE) { + navigation.run(); + return; + } + + boolean accept = + behaviour == UnexpectedAlertBehaviour.ACCEPT + || behaviour == UnexpectedAlertBehaviour.ACCEPT_AND_NOTIFY; + + BiDi bidi = ((HasBiDi) this).getBiDi(); + long listenerId = + bidi.addListener( + contextId, + USER_PROMPT_OPENED_EVENT, + prompt -> { + if (contextId.equals(prompt.getBrowsingContextId())) { + LOG.fine( + () -> + String.format( + "Handling %s user prompt during BiDi navigation (%s)", + prompt.getType(), accept ? "accept" : "dismiss")); + new BrowsingContext(this, contextId).handleUserPrompt(accept); + } + }); + try { + navigation.run(); + } finally { + bidi.removeListener(listenerId); + } + } + @Override public String getTitle() { Response response = execute(DriverCommand.GET_TITLE); @@ -1245,7 +1316,9 @@ private class RemoteNavigation implements Navigation { @Override public void back() { if (isBiDiEnabled()) { - new BrowsingContext(RemoteWebDriver.this, getWindowHandle()).back(); + String contextId = getWindowHandle(); + navigateViaBiDi( + contextId, () -> new BrowsingContext(RemoteWebDriver.this, contextId).back()); } else { execute(DriverCommand.GO_BACK); } @@ -1254,7 +1327,9 @@ public void back() { @Override public void forward() { if (isBiDiEnabled()) { - new BrowsingContext(RemoteWebDriver.this, getWindowHandle()).forward(); + String contextId = getWindowHandle(); + navigateViaBiDi( + contextId, () -> new BrowsingContext(RemoteWebDriver.this, contextId).forward()); } else { execute(DriverCommand.GO_FORWARD); } @@ -1273,7 +1348,11 @@ public void to(URL url) { @Override public void refresh() { if (isBiDiEnabled()) { - new BrowsingContext(RemoteWebDriver.this, getWindowHandle()).reload(getReadinessState()); + String contextId = getWindowHandle(); + navigateViaBiDi( + contextId, + () -> + new BrowsingContext(RemoteWebDriver.this, contextId).reload(getReadinessState())); } else { execute(DriverCommand.REFRESH); } From 8ccc2cbd209d6b7b52e21e0b3986f5ae923ed198 Mon Sep 17 00:00:00 2001 From: Krishna Date: Sat, 21 Mar 2026 20:44:44 -0400 Subject: [PATCH 5/5] [java][bidi] Honor pageLoadTimeout in BiDi navigation and fix prompt handling --- .../bidi/browsingcontext/BrowsingContext.java | 7 ++ .../selenium/remote/RemoteWebDriver.java | 97 +++++++++++++------ 2 files changed, 77 insertions(+), 27 deletions(-) diff --git a/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java b/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java index 4b7002d7f1c2d..758346e6e93e5 100644 --- a/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java +++ b/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java @@ -200,6 +200,13 @@ public NavigationResult reload(ReadinessState readinessState) { RELOAD, Map.of(CONTEXT, id, "wait", readinessState.toString()), navigationInfoMapper)); } + public NavigationResult reload(ReadinessState readinessState, Duration timeout) { + return this.bidi.send( + new Command<>( + RELOAD, Map.of(CONTEXT, id, "wait", readinessState.toString()), navigationInfoMapper), + timeout); + } + // Yet to be implemented by browser vendors private NavigationResult reload(boolean ignoreCache, ReadinessState readinessState) { return this.bidi.send( diff --git a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java index 87c4f4053b79e..7c699a58ebcd9 100644 --- a/java/src/org/openqa/selenium/remote/RemoteWebDriver.java +++ b/java/src/org/openqa/selenium/remote/RemoteWebDriver.java @@ -42,6 +42,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; import java.util.logging.Level; import java.util.logging.Logger; @@ -149,6 +150,18 @@ public class RemoteWebDriver private Logs remoteLogs; + // Cached page-load timeout used by BiDi navigation. Null until set by the user via + // pageLoadTimeout() or lazily populated from the session's GET_TIMEOUTS response. + private volatile Duration biDiPageLoadTimeout = null; + + // Set to true once the one-time browsingContext.userPromptOpened listener is installed. + // Sending session.subscribe once is sufficient; subsequent navigations reuse it. + private final AtomicBoolean biDiPromptListenerInstalled = new AtomicBoolean(false); + + // Non-null only while a BiDi navigation is in progress. The prompt handler uses this to + // ignore events that arrive outside of a navigation (e.g., user-triggered alerts). + private volatile String biDiNavigatingContextId = null; + @SuppressWarnings("deprecation") private LocalLogs localLogs; @@ -381,9 +394,11 @@ public Capabilities getCapabilities() { public void get(String url) { if (isBiDiEnabled()) { String contextId = getWindowHandle(); + ReadinessState readiness = getReadinessState(); + Duration timeout = getPageLoadDuration(); navigateViaBiDi( contextId, - () -> new BrowsingContext(this, contextId).navigate(url, getReadinessState())); + () -> new BrowsingContext(this, contextId).navigate(url, readiness, timeout)); } else { execute(DriverCommand.GET(url)); } @@ -412,6 +427,19 @@ private ReadinessState getReadinessState() { return ReadinessState.COMPLETE; } + // Returns the effective page load timeout for BiDi navigation commands. The value is cached so + // repeated navigations don't incur an extra HTTP round-trip to GET_TIMEOUTS. + private Duration getPageLoadDuration() { + if (biDiPageLoadTimeout == null) { + synchronized (this) { + if (biDiPageLoadTimeout == null) { + biDiPageLoadTimeout = manage().timeouts().getPageLoadTimeout(); + } + } + } + return biDiPageLoadTimeout; + } + private static final Json BIDI_JSON = new Json(); // Shared event definition for browsingContext.userPromptOpened used during navigation. @@ -437,40 +465,51 @@ private UnexpectedAlertBehaviour getUnhandledPromptBehaviour() { return UnexpectedAlertBehaviour.DISMISS_AND_NOTIFY; } - // Wraps a BiDi navigation call with a userPromptOpened listener that handles any alerts - // according to the unhandledPromptBehavior capability. This replicates, for BiDi navigation, - // the automatic prompt handling that classic WebDriver delegates to the browser via the - // capability. - private void navigateViaBiDi(String contextId, Runnable navigation) { - UnexpectedAlertBehaviour behaviour = getUnhandledPromptBehaviour(); - if (behaviour == UnexpectedAlertBehaviour.IGNORE) { - navigation.run(); - return; - } - - boolean accept = - behaviour == UnexpectedAlertBehaviour.ACCEPT - || behaviour == UnexpectedAlertBehaviour.ACCEPT_AND_NOTIFY; - - BiDi bidi = ((HasBiDi) this).getBiDi(); - long listenerId = - bidi.addListener( - contextId, - USER_PROMPT_OPENED_EVENT, - prompt -> { - if (contextId.equals(prompt.getBrowsingContextId())) { + // Installs a single session-scoped browsingContext.userPromptOpened listener the first time + // BiDi navigation is used. The listener only acts while biDiNavigatingContextId is set, + // so it has no effect on user-triggered alerts outside of navigation. + private void ensureBiDiPromptListener() { + if (biDiPromptListenerInstalled.compareAndSet(false, true)) { + ((HasBiDi) this) + .getBiDi() + .addListener( + USER_PROMPT_OPENED_EVENT, + prompt -> { + String contextId = biDiNavigatingContextId; + if (contextId == null || !contextId.equals(prompt.getBrowsingContextId())) { + return; + } + UnexpectedAlertBehaviour behaviour = getUnhandledPromptBehaviour(); + if (behaviour == UnexpectedAlertBehaviour.IGNORE) { + return; + } + boolean accept = + behaviour == UnexpectedAlertBehaviour.ACCEPT + || behaviour == UnexpectedAlertBehaviour.ACCEPT_AND_NOTIFY; LOG.fine( () -> String.format( "Handling %s user prompt during BiDi navigation (%s)", prompt.getType(), accept ? "accept" : "dismiss")); new BrowsingContext(this, contextId).handleUserPrompt(accept); - } - }); + }); + } + } + + // Wraps a BiDi navigation call with prompt handling that replicates, for BiDi, the automatic + // unhandledPromptBehavior that classic WebDriver delegates to the browser. + private void navigateViaBiDi(String contextId, Runnable navigation) { + ensureBiDiPromptListener(); + UnexpectedAlertBehaviour behaviour = getUnhandledPromptBehaviour(); + if (behaviour == UnexpectedAlertBehaviour.IGNORE) { + navigation.run(); + return; + } + biDiNavigatingContextId = contextId; try { navigation.run(); } finally { - bidi.removeListener(listenerId); + biDiNavigatingContextId = null; } } @@ -1242,6 +1281,7 @@ public Duration getScriptTimeout() { @Override public Timeouts pageLoadTimeout(Duration duration) { execute(DriverCommand.SET_PAGE_LOAD_TIMEOUT(duration)); + biDiPageLoadTimeout = duration; return this; } @@ -1349,10 +1389,13 @@ public void to(URL url) { public void refresh() { if (isBiDiEnabled()) { String contextId = getWindowHandle(); + ReadinessState readiness = getReadinessState(); + Duration timeout = getPageLoadDuration(); navigateViaBiDi( contextId, () -> - new BrowsingContext(RemoteWebDriver.this, contextId).reload(getReadinessState())); + new BrowsingContext(RemoteWebDriver.this, contextId) + .reload(readiness, timeout)); } else { execute(DriverCommand.REFRESH); }