diff --git a/.idea/misc.xml b/.idea/misc.xml
index 43262c34163c2..24c10f29685da 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,9 +1,10 @@
+
-
+
-
+
\ No newline at end of file
diff --git a/java/src/org/openqa/selenium/bidi/BiDi.java b/java/src/org/openqa/selenium/bidi/BiDi.java
index 50b4ae35ca4f3..85205aa0e90ee 100644
--- a/java/src/org/openqa/selenium/bidi/BiDi.java
+++ b/java/src/org/openqa/selenium/bidi/BiDi.java
@@ -21,6 +21,8 @@
import java.io.Closeable;
import java.time.Duration;
+import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -34,6 +36,7 @@ public class BiDi implements Closeable {
private final Duration timeout;
private final Connection connection;
+ private final Map, List> contextListenerIds = new HashMap<>();
/**
* @deprecated Use constructor with timeout parameter: {@link #BiDi(Connection, Duration)}
@@ -104,7 +107,7 @@ public long addListener(String browsingContextId, Event event, Consumer long addListener(Set browsingContextIds, Event event, Consumer handler) {
- Require.nonNull("List of browsing context ids", browsingContextIds);
+ Require.nonEmpty("List of browsing context ids", browsingContextIds);
Require.nonNull("Event to listen for", event);
Require.nonNull("Handler to call", handler);
@@ -113,7 +116,9 @@ public long addListener(Set browsingContextIds, Event event, Cons
"session.subscribe",
Map.of("contexts", browsingContextIds, "events", List.of(event.getMethod()))));
- return connection.addListener(event, handler);
+ long id = connection.addListener(event, handler);
+ contextListenerIds.computeIfAbsent(event, k -> new ArrayList<>()).add(id);
+ return id;
}
public void clearListener(Event event) {
@@ -127,6 +132,32 @@ public void clearListener(Event event) {
}
}
+ public void clearListener(Set browsingContextIds, Event event) {
+ Require.nonEmpty("List of browsing context ids", browsingContextIds);
+ Require.nonNull("Event to listen for", event);
+
+ // The browser throws an error if we try to unsubscribe an event that was not subscribed in the
+ // first place
+ if (!connection.isEventSubscribed(event)) {
+ return;
+ }
+
+ List ids = contextListenerIds.remove(event);
+ if (ids != null && !ids.isEmpty()) {
+ // Subscription was context-specific: unsubscribe only for those contexts.
+ send(
+ new Command<>(
+ "session.unsubscribe",
+ Map.of("contexts", browsingContextIds, "events", List.of(event.getMethod()))));
+ ids.forEach(connection::removeListener);
+ } else {
+ // Subscription was global: context-specific unsubscription is invalid per the BiDi protocol,
+ // so fall back to a global unsubscription.
+ send(new Command<>("session.unsubscribe", Map.of("events", List.of(event.getMethod()))));
+ connection.clearListener(event);
+ }
+ }
+
public void removeListener(long id) {
connection.removeListener(id);
}
diff --git a/java/src/org/openqa/selenium/bidi/module/BrowsingContextInspector.java b/java/src/org/openqa/selenium/bidi/module/BrowsingContextInspector.java
index d37e0df91a064..78bb6ae73460e 100644
--- a/java/src/org/openqa/selenium/bidi/module/BrowsingContextInspector.java
+++ b/java/src/org/openqa/selenium/bidi/module/BrowsingContextInspector.java
@@ -20,6 +20,7 @@
import java.io.StringReader;
import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
@@ -240,6 +241,27 @@ private void addNavigationEventListener(String name, Consumer co
}
}
+ public void clearListener(String browsingContextId) {
+ Require.nonNull("Browsing context id", browsingContextId);
+ clearListeners(Collections.singleton(browsingContextId));
+ }
+
+ public void clearListeners(Set browsingContextIds) {
+ Require.nonNull("Browsing context id list", browsingContextIds);
+
+ List.of(
+ browsingContextCreated,
+ browsingContextDestroyed,
+ userPromptOpened,
+ userPromptClosed,
+ historyUpdated,
+ downloadWillBeginEvent,
+ downloadEndEvent)
+ .forEach(event -> this.bidi.clearListener(browsingContextIds, event));
+
+ navigationEventSet.forEach(event -> this.bidi.clearListener(browsingContextIds, event));
+ }
+
@Override
public void close() {
this.bidi.clearListener(browsingContextCreated);
diff --git a/java/src/org/openqa/selenium/bidi/module/LogInspector.java b/java/src/org/openqa/selenium/bidi/module/LogInspector.java
index cc1d30f8bf7b2..26fb5415ffe14 100644
--- a/java/src/org/openqa/selenium/bidi/module/LogInspector.java
+++ b/java/src/org/openqa/selenium/bidi/module/LogInspector.java
@@ -152,8 +152,18 @@ private long addLogEntryAddedListener(Consumer consumer) {
}
}
+ public void clearListener(String browsingContextId) {
+ Require.nonNull("Browsing context id", browsingContextId);
+ clearListeners(Collections.singleton(browsingContextId));
+ }
+
+ public void clearListeners(Set browsingContextIds) {
+ Require.nonNull("Browsing context id list", browsingContextIds);
+ this.bidi.clearListener(browsingContextIds, this.logEntryAddedEvent);
+ }
+
@Override
public void close() {
- this.bidi.clearListener(Log.entryAdded());
+ this.bidi.clearListener(this.logEntryAddedEvent);
}
}
diff --git a/java/src/org/openqa/selenium/bidi/module/SpeculationInspector.java b/java/src/org/openqa/selenium/bidi/module/SpeculationInspector.java
index 30b0ea059bde0..1a8954ec00fbb 100644
--- a/java/src/org/openqa/selenium/bidi/module/SpeculationInspector.java
+++ b/java/src/org/openqa/selenium/bidi/module/SpeculationInspector.java
@@ -69,8 +69,18 @@ public void removeListener(long subscriptionId) {
this.bidi.removeListener(subscriptionId);
}
+ public void clearListener(String browsingContextId) {
+ Require.nonNull("Browsing context id", browsingContextId);
+ clearListeners(Collections.singleton(browsingContextId));
+ }
+
+ public void clearListeners(Set browsingContextIds) {
+ Require.nonNull("Browsing context id list", browsingContextIds);
+ this.bidi.clearListener(browsingContextIds, this.prefetchStatusUpdatedEvent);
+ }
+
@Override
public void close() {
- this.bidi.clearListener(Speculation.prefetchStatusUpdated());
+ this.bidi.clearListener(this.prefetchStatusUpdatedEvent);
}
}
diff --git a/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextInspectorTest.java b/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextInspectorTest.java
index 46045194da2a0..c9b7ba10d5c14 100644
--- a/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextInspectorTest.java
+++ b/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextInspectorTest.java
@@ -24,11 +24,15 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.openqa.selenium.testing.drivers.Browser.FIREFOX;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -318,6 +322,71 @@ void canListenToNavigationFailedEvent()
}
}
+ @Test
+ @NeedsFreshDriver
+ void canClearListenersForBrowsingContext()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ try (BrowsingContextInspector inspector = new BrowsingContextInspector(driver)) {
+ CompletableFuture future = new CompletableFuture<>();
+ inspector.onDomContentLoaded(future::complete);
+
+ BrowsingContext context = new BrowsingContext(driver, driver.getWindowHandle());
+ context.navigate(appServer.whereIs("/bidi/logEntryAdded.html"), ReadinessState.COMPLETE);
+
+ NavigationInfo navigationInfo = future.get(5, TimeUnit.SECONDS);
+ assertThat(navigationInfo.getBrowsingContextId()).isEqualTo(context.getId());
+
+ // Clear listeners for this browsing context
+ inspector.clearListener(context.getId());
+
+ // Re-subscribe and verify events still work after re-subscribing
+ CompletableFuture newFuture = new CompletableFuture<>();
+ inspector.onDomContentLoaded(newFuture::complete);
+
+ context.navigate(appServer.whereIs("/simpleTest.html"), ReadinessState.COMPLETE);
+
+ NavigationInfo newNavigationInfo = newFuture.get(5, TimeUnit.SECONDS);
+ assertThat(newNavigationInfo.getBrowsingContextId()).isEqualTo(context.getId());
+ assertThat(newNavigationInfo.getUrl()).contains("/simpleTest.html");
+ }
+ }
+
+ @Test
+ @NeedsFreshDriver
+ void canClearListenersForMultipleBrowsingContexts()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ Set browsingContextIds = new HashSet<>();
+ browsingContextIds.add(driver.getWindowHandle());
+
+ try (BrowsingContextInspector inspector = new BrowsingContextInspector(driver)) {
+ List receivedEvents = new ArrayList<>();
+ CountDownLatch latch = new CountDownLatch(1);
+ inspector.onNavigationStarted(
+ info -> {
+ receivedEvents.add(info);
+ latch.countDown();
+ });
+
+ BrowsingContext context = new BrowsingContext(driver, driver.getWindowHandle());
+ context.navigate(appServer.whereIs("/bidi/logEntryAdded.html"), ReadinessState.COMPLETE);
+
+ latch.await(5, TimeUnit.SECONDS);
+ assertThat(receivedEvents).hasSizeGreaterThanOrEqualTo(1);
+
+ // Clear listeners for the set of browsing context ids
+ inspector.clearListeners(browsingContextIds);
+
+ // Re-subscribe with a new listener after clearing
+ CompletableFuture newFuture = new CompletableFuture<>();
+ inspector.onNavigationStarted(newFuture::complete);
+
+ context.navigate(appServer.whereIs("/simpleTest.html"), ReadinessState.COMPLETE);
+
+ NavigationInfo newNavigationInfo = newFuture.get(5, TimeUnit.SECONDS);
+ assertThat(newNavigationInfo.getBrowsingContextId()).isEqualTo(context.getId());
+ }
+ }
+
@Test
@NeedsFreshDriver
void canListenToHistoryUpdatedEvent()
diff --git a/java/test/org/openqa/selenium/bidi/log/LogInspectorTest.java b/java/test/org/openqa/selenium/bidi/log/LogInspectorTest.java
index 35ddedfaf8a58..bff1d5ee11e22 100644
--- a/java/test/org/openqa/selenium/bidi/log/LogInspectorTest.java
+++ b/java/test/org/openqa/selenium/bidi/log/LogInspectorTest.java
@@ -20,7 +20,9 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import java.util.ArrayList;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
@@ -430,6 +432,74 @@ void canListenToAnyTypeOfLogForMultipleBrowsingContexts() throws InterruptedExce
}
}
+ @Test
+ @NeedsFreshDriver
+ void canClearListenersForBrowsingContext()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ String browsingContextId = driver.getWindowHandle();
+ try (LogInspector logInspector = new LogInspector(driver)) {
+ CompletableFuture future = new CompletableFuture<>();
+ logInspector.onConsoleEntry(future::complete);
+
+ page = appServer.whereIs("/bidi/logEntryAdded.html");
+ driver.get(page);
+ driver.findElement(By.id("consoleLog")).click();
+
+ ConsoleLogEntry logEntry = future.get(5, TimeUnit.SECONDS);
+ assertThat(logEntry.getText()).isEqualTo("Hello, world!");
+
+ // Clear listeners for this browsing context
+ logInspector.clearListener(browsingContextId);
+
+ // Re-subscribe and verify events still work after re-subscribing
+ CompletableFuture newFuture = new CompletableFuture<>();
+ logInspector.onConsoleEntry(newFuture::complete);
+
+ driver.findElement(By.id("consoleLog")).click();
+
+ ConsoleLogEntry newLogEntry = newFuture.get(5, TimeUnit.SECONDS);
+ assertThat(newLogEntry.getText()).isEqualTo("Hello, world!");
+ }
+ }
+
+ @Test
+ @NeedsFreshDriver
+ void canClearListenersForMultipleBrowsingContexts()
+ throws ExecutionException, InterruptedException, TimeoutException {
+ Set browsingContextIds = new HashSet<>();
+ browsingContextIds.add(driver.getWindowHandle());
+
+ try (LogInspector logInspector = new LogInspector(driver)) {
+ List receivedEntries = new ArrayList<>();
+ CountDownLatch latch = new CountDownLatch(1);
+ logInspector.onConsoleEntry(
+ entry -> {
+ receivedEntries.add(entry);
+ latch.countDown();
+ });
+
+ page = appServer.whereIs("/bidi/logEntryAdded.html");
+ driver.get(page);
+ driver.findElement(By.id("consoleLog")).click();
+
+ latch.await(5, TimeUnit.SECONDS);
+ assertThat(receivedEntries).hasSize(1);
+ assertThat(receivedEntries.get(0).getText()).isEqualTo("Hello, world!");
+
+ // Clear listeners for the set of browsing context ids
+ logInspector.clearListeners(browsingContextIds);
+
+ // Re-subscribe with a new listener after clearing
+ CompletableFuture newFuture = new CompletableFuture<>();
+ logInspector.onConsoleEntry(newFuture::complete);
+
+ driver.findElement(By.id("consoleLog")).click();
+
+ ConsoleLogEntry newLogEntry = newFuture.get(5, TimeUnit.SECONDS);
+ assertThat(newLogEntry.getText()).isEqualTo("Hello, world!");
+ }
+ }
+
@Test
@NeedsFreshDriver
void canListenToLogsWithMultipleConsumers()
diff --git a/java/test/org/openqa/selenium/bidi/speculation/SpeculationInspectorTest.java b/java/test/org/openqa/selenium/bidi/speculation/SpeculationInspectorTest.java
index 459a3e058bbe3..249db36281109 100644
--- a/java/test/org/openqa/selenium/bidi/speculation/SpeculationInspectorTest.java
+++ b/java/test/org/openqa/selenium/bidi/speculation/SpeculationInspectorTest.java
@@ -22,8 +22,10 @@
import static org.openqa.selenium.testing.drivers.Browser.SAFARI;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
@@ -223,6 +225,118 @@ void canListenToPrefetchStatusUpdatedWithFailureEvents() throws InterruptedExcep
assertThat(firstEvent.getStatus()).isIn(PreloadingStatus.PENDING, PreloadingStatus.FAILURE);
}
+ @Test
+ @NeedsFreshDriver
+ @NotYetImplemented(FIREFOX)
+ @NotYetImplemented(SAFARI)
+ void canClearListenersForBrowsingContext() throws InterruptedException {
+ CountDownLatch latch = new CountDownLatch(1);
+ List events = new ArrayList<>();
+
+ speculationInspector.onPrefetchStatusUpdated(
+ event -> {
+ events.add(event);
+ latch.countDown();
+ });
+
+ String testUrl = appServer.whereIs("/common/blank.html");
+ driver.get(testUrl);
+
+ String prefetchTarget = appServer.whereIs("/common/dummy.xml");
+ String speculationRules =
+ String.format(
+ "{\"prefetch\": [{\"source\": \"list\", \"urls\": [\"%s\"]}]}", prefetchTarget);
+
+ addSpeculationRulesAndLink(speculationRules, prefetchTarget, "Test Link", "prefetch-page");
+
+ latch.await(5, TimeUnit.SECONDS);
+ assertThat(events).hasSizeGreaterThanOrEqualTo(1);
+
+ // Clear listeners for this browsing context
+ speculationInspector.clearListener(driver.getWindowHandle());
+
+ // Re-subscribe after clearing
+ CountDownLatch newLatch = new CountDownLatch(1);
+ List newEvents = new ArrayList<>();
+
+ speculationInspector.onPrefetchStatusUpdated(
+ event -> {
+ newEvents.add(event);
+ newLatch.countDown();
+ });
+
+ driver.get(testUrl);
+
+ String prefetchTarget2 = appServer.whereIs("/common/square.png");
+ String speculationRules2 =
+ String.format(
+ "{\"prefetch\": [{\"source\": \"list\", \"urls\": [\"%s\"]}]}", prefetchTarget2);
+
+ addSpeculationRulesAndLink(
+ speculationRules2, prefetchTarget2, "Test Link 2", "prefetch-page-2");
+
+ newLatch.await(5, TimeUnit.SECONDS);
+ assertThat(newEvents).hasSizeGreaterThanOrEqualTo(1);
+ assertThat(newEvents.get(0).getUrl()).isEqualTo(prefetchTarget2);
+ }
+
+ @Test
+ @NeedsFreshDriver
+ @NotYetImplemented(FIREFOX)
+ @NotYetImplemented(SAFARI)
+ void canClearListenersForMultipleBrowsingContexts() throws InterruptedException {
+ CountDownLatch latch = new CountDownLatch(1);
+ List events = new ArrayList<>();
+
+ speculationInspector.onPrefetchStatusUpdated(
+ event -> {
+ events.add(event);
+ latch.countDown();
+ });
+
+ String testUrl = appServer.whereIs("/common/blank.html");
+ driver.get(testUrl);
+
+ String prefetchTarget = appServer.whereIs("/common/dummy.xml");
+ String speculationRules =
+ String.format(
+ "{\"prefetch\": [{\"source\": \"list\", \"urls\": [\"%s\"]}]}", prefetchTarget);
+
+ addSpeculationRulesAndLink(speculationRules, prefetchTarget, "Test Link", "prefetch-page");
+
+ latch.await(5, TimeUnit.SECONDS);
+ assertThat(events).hasSizeGreaterThanOrEqualTo(1);
+
+ // Clear listeners for the set of browsing context ids
+ Set browsingContextIds = new HashSet();
+ browsingContextIds.add(driver.getWindowHandle());
+ speculationInspector.clearListeners(browsingContextIds);
+
+ // Re-subscribe after clearing
+ CountDownLatch newLatch = new CountDownLatch(1);
+ List newEvents = new ArrayList<>();
+
+ speculationInspector.onPrefetchStatusUpdated(
+ event -> {
+ newEvents.add(event);
+ newLatch.countDown();
+ });
+
+ driver.get(testUrl);
+
+ String prefetchTarget2 = appServer.whereIs("/common/square.png");
+ String speculationRules2 =
+ String.format(
+ "{\"prefetch\": [{\"source\": \"list\", \"urls\": [\"%s\"]}]}", prefetchTarget2);
+
+ addSpeculationRulesAndLink(
+ speculationRules2, prefetchTarget2, "Test Link 2", "prefetch-page-2");
+
+ newLatch.await(5, TimeUnit.SECONDS);
+ assertThat(newEvents).hasSizeGreaterThanOrEqualTo(1);
+ assertThat(newEvents.get(0).getUrl()).isEqualTo(prefetchTarget2);
+ }
+
@Test
@NeedsFreshDriver
@NotYetImplemented(FIREFOX)